<!DOCTYPE html>
<html lang="zh-cn">
  <head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="author" content="Zhou Wei <zromyk@163.com>">
  <title>Linux C</title>
  <link rel="shortcut icon" href="/favicon.ico">
  <link rel="stylesheet" href="/style/html/pure.css">
  <link rel="stylesheet" href="/style/html/main.css">
  <link rel="stylesheet" href="https://cdn.staticfile.org/font-awesome/4.7.0/css/font-awesome.css">
  <!-- <link rel="stylesheet" href="https://apps.bdimg.com/libs/highlight.js/9.1.0/styles/default.min.css"> -->
<link rel="stylesheet" href="/style/article/highlight/default.min.css">
<link rel="stylesheet" href="/style/article/pell-1.0.6/dist/pell.css">

</head>
<body>
  <div id="menu-background"></div>
  <div id="menu">
    <div class="pure-menu pure-menu-horizontal">
  <ul class="pure-menu-list block-middle">
    <li class="pure-menu-item">
  <a class="pure-menu-heading" href="/index.html">ZROMYK</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link" href="/index.html">主页</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link" href="/public/archive/index.html">归档</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link" href="/public/download/index.html">下载</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link" href="/public/feedback/index.html">反馈</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link" href="/public/about/index.html">关于我</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link" href="https://github.com/zromyk"><i class="fa fa-github" style="font-size:32px"></i></a>
</li>

  </ul>
</div>

  </div>
  <div id="layout">
    <div class="content">
      <div id="nav">
    <div id="navigation" class="navigation">
  <ul class="pure-menu-list">
    <li class="pure-menu-item">
  <a class="pure-menu-link nav2" onclick="animateByNav()" href="#man-page">一、Man Page</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav2" onclick="animateByNav()" href="#gdb">二、GDB 调试</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav2" onclick="animateByNav()" href="#_1">三、变量的存储布局</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav2" onclick="animateByNav()" href="#c">四、C内联汇编</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav2" onclick="animateByNav()" href="#_2">五、多目标文件的链接</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#51">5.1 定义和声明</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav4" onclick="animateByNav()" href="#externstatic"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>extern</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>static</code></span>关键字</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#52">5.2 静态库</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#_3">那么链接共享库和链接静态库有什么区别呢？</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#53">5.3 共享库</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#-fpic">那么用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>-fPIC</code></span>生成的目标文件和一般的目标文件有什么不同呢？</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav2" onclick="animateByNav()" href="#_4">六、预处理</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#61">6.1 函数式宏定义</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#62-c99">6.2 内联函数（C99）</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#63">6.3 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>#</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>##</code></span>运算符和可变参数</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav2" onclick="animateByNav()" href="#_5">算法</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav2" onclick="animateByNav()" href="#c_1">C语言关键字</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#volatile"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>volatile</code></span> 限定符</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#gcc"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>gcc</code></span> 的编译优化选项</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#restrict"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>restrict</code></span></a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav2" onclick="animateByNav()" href="#28-io">第 28 章 文件与I/O</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#1-hello-world">1. 汇编程序的Hello world</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#2-ciounbuffered-io">2. C标准I/O库函数与Unbuffered I/O函数</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#3-openclose">3. open/close</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#printf">printf 重定向</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#4-readwrite">4. read/write</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#5-lseek">5. lseek</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#6-fcntl">6. fcntl</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#7-ioctl">7. ioctl</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#8-mmap">8. mmap</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav2" onclick="animateByNav()" href="#29">第 29 章 文件系统</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#2-ext2">2. ext2文件系统</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#21">2.1. 总体存储布局</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#22">2.2. 实例剖析</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#23">2.3. 数据块寻址</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#24">2.4. 文件和目录操作的系统函数</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#3-vfs">3. VFS</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#31">3.1. 内核数据结构</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#32-dupdup2">3.2. dup和dup2函数</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav2" onclick="animateByNav()" href="#30">30. 进程</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#1">1. 引言</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#2">2. 环境变量</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#21_1">2.1 一些比较重要的环境变量</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#22-getenv">2.2 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>getenv</code></span>函数</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#23-setenv">2.3 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>setenv</code></span>函数</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#3">3. 进程控制</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#31-fork">3.1. fork函数</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#32-exec">3.2. exec函数</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#33-waitwaitpid">3.3. wait和waitpid函数</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#4">4. 进程间通信</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#41">4.1. 管道</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#42-ipc">4.2. 其它IPC机制</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#5-shell">5. 练习：实现简单的Shell</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav2" onclick="animateByNav()" href="#31-shell">31. Shell脚本</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#1-shell">1. Shell的历史</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#2-shell">2. Shell如何执行命令</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#21_2">2.1. 执行交互式命令</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#22_1">2.2. 执行脚本</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#3-shell">3. Shell的基本语法</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#31_1">3.1. 变量</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#32-globbing">3.2. 文件名代换（Globbing）：* ? <a href="https://blog.csdn.net/u012365926/article/details/114892795?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-0&amp;spm=1001.2101.3001.4242" title="C++标准库 STL —— 仿函数和适配器"></a></a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#33">3.3. 命令代换：`或 $()</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#34">3.4. 算术代换：$(())</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#36">3.6. 单引号</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#37">3.7. 双引号</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#4-bash">4. bash启动脚本</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#41-shell-login">4.1. 作为交互登录Shell启动，或者使用&ndash;login参数启动</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#42-shell">4.2. 以交互非登录Shell启动</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#43">4.3. 非交互启动</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#44-sh">4.4. 以sh命令启动</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#5-shell_1">5. Shell脚本语法</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#51-test">5.1. 条件测试：test [</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#52-ifthenelifelsefi">5.2. if/then/elif/else/fi</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#53-caseesac">5.3. case/esac</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#54-fordodone">5.4. for/do/done</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#55-whiledodone">5.5. while/do/done</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#56">5.6. 位置参数和特殊变量</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#57">5.7. 函数</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#6-shell">6. Shell脚本的调试方法</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav2" onclick="animateByNav()" href="#32">32. 正则表达式</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#1_1">1. 引言</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#2_1">2. 基本语法</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#3-sed">3. sed</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#4-awk">4. awk</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#5-c">5. 练习：在C语言中使用正则表达式</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav2" onclick="animateByNav()" href="#33_1">33. 信号</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#1_2">1. 信号的基本概念</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#2_2">2. 产生信号</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#21_3">2.1. 通过终端按键产生信号</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#22_2">2.2. 调用系统函数向进程发信号</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#23_1">2.3. 由软件条件产生信号</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#3_1">3. 阻塞信号</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#31_2">3.1. 信号在内核中的表示</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#32_1">3.2. 信号集操作函数</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#33-sigprocmask">3.3. sigprocmask</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#34-sigpending">3.4. sigpending</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#4_1">4. 捕捉信号</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#41_1">4.1. 内核如何实现信号的捕捉</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#42-sigaction">4.2. sigaction</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#43-pause">4.3. pause</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#44">4.4. 可重入函数</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#45-sigatomictvolatile">4.5. sig<em>atomic</em>t类型与volatile限定符</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#46-sigsuspend">4.6. 竞态条件与sigsuspend函数</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#47-sigchld">4.7. 关于SIGCHLD信号</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav2" onclick="animateByNav()" href="#34_1">34. 终端、作业控制与守护进程</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#1_3">1. 终端</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#11">1.1. 终端的基本概念</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#12">1.2. 终端登录过程</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#13">1.3. 网络登录过程</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#2_3">2. 作业控制</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#21-session">2.1. Session与进程组</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#22_3">2.2. 与作业控制有关的信号</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#3_2">3. 守护进程</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav2" onclick="animateByNav()" href="#35">35. 线程</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#1_4">1. 线程的概念</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#2_4">2. 线程控制</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#21_4">2.1. 创建线程</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#22_4">2.2. 终止线程</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#3_3">3. 线程间同步</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#31-mutex">3.1. mutex（互斥锁）</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#32-condition-variable">3.2. Condition Variable（条件变量）</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#33-semaphore">3.3. Semaphore（信号量）</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#34_2">3.4. 其它线程间同步机制</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#4_2">4. 编程练习</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav2" onclick="animateByNav()" href="#36-tcpip">36. TCP/IP协议基础</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#1-tcpip">1. TCP/IP协议栈与数据包封装</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#2-rfc-894">2. 以太网(RFC 894)帧格式</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#3-arp">3. ARP数据报格式</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#4-ip">4. IP数据报格式</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#5-ip">5. IP地址与路由</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#6-udp">6. UDP段格式</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#7-tcp">7. TCP协议</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#71">7.1. 段格式</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#72">7.2. 通讯时序</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#73">7.3. 流量控制</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav2" onclick="animateByNav()" href="#37-socket">37. socket编程</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#1_5">1. 预备知识</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#11_1">1.1. 网络字节序</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#12-socket">1.2. socket地址的数据类型及相关函数</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#2-tcp">2. 基于TCP协议的网络程序</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#21-tcp">2.1. 最简单的TCP网络程序</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#22_5">2.2. 错误处理与读写控制</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#23-client">2.3. 把client改为交互式输入</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#24-forkclient">2.4. 使用fork并发处理多个client的请求</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#25-setsockopt">2.5. setsockopt</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#26-select">2.6. 使用select</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#3-udp">3. 基于UDP协议的网络程序</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#4-unix-domain-socket-ipc">4. UNIX Domain Socket IPC</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav3" onclick="animateByNav()" href="#5-web">5. 练习：实现简单的Web服务器</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#51-http">5.1. 基本HTTP协议</a>
</li>
<li class="pure-menu-item">
  <a class="pure-menu-link nav5" onclick="animateByNav()" href="#52-cgi">5.2. 执行CGI程序</a>
</li>

  </ul>
</div>

</div>
<div id="content-articles">
  <h1 id="Linux C" class="content-subhead">Linux C</h1>
  <p>
    <span>2021-04-04</span>
    <span><span class="post-category post-category-c">C</span></span>
    <button id="button-markdownEditor" class="pure-button" onclick="markdownEditor()">启用编辑</button>
    <button id="button-save" class="pure-button" onclick="save()">保存</button>
  </p>
  <div id="content-articles-markdownEditor" style="display: none;">
    <h1>编辑 power by pell</h1>
    <div id="editor" class="pell"></div>
    <div style="margin-top:20px;">
        <h3>Text output:</h3>
        <div id="text-output"></div>
    </div>
    <div style="margin-top:20px;">
        <h3>HTML output:</h3>
        <pre id="html-output"></pre>
    </div>
  </div>
  <div id="content-articles-markdown">
    <blockquote class="content-quote">
<p>转载：<a href="http://akaedu.github.io/book/">Linux C编程一站式学习</a></p>
</blockquote>
<h2 id="man-page">一、Man Page</h2>
<p>Man Page 是 Linux 开发最常用的参考手册，由很多页面组成，每个页面描述一个主题，这些页面被组织成若干个 Section。FHS（Filesystem Hierarchy Standard）标准规定了 Man Page 各 Section 的含义如下：</p>
<p><strong>表 3.1. Man Page的Section</strong></p>
<div class="pure-table-scrollable"><table class="pure-table pure-table-horizontal">
<thead>
<tr>
<th>Section</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>用户命令，例如 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>ls(1)</code></span></td>
</tr>
<tr>
<td>2</td>
<td>系统调用，例如 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>_exit(2)</code></span></td>
</tr>
<tr>
<td>3</td>
<td>库函数，例如 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>printf(3)</code></span></td>
</tr>
<tr>
<td>4</td>
<td>特殊文件，例如 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>null(4)</code></span> 描述了设备文件<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/dev/null</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/dev/zero</code></span> 的作用</td>
</tr>
<tr>
<td>5</td>
<td>系统配置文件的格式，例如 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>passwd(5)</code></span> 描述了系统配置文件 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>/etc/passwd</code></span> 的格式</td>
</tr>
<tr>
<td>6</td>
<td>游戏</td>
</tr>
<tr>
<td>7</td>
<td>其它杂项，例如 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>bash-builtins(7)</code></span> 描述了 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>bash</code></span> 的各种内建命令</td>
</tr>
<tr>
<td>8</td>
<td>系统管理命令，例如 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>ifconfig(8)</code></span></td>
</tr>
</tbody>
</table></div>
<p>注意区分用户命令和系统管理命令，用户命令通常位于<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/bin</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/usr/bin</code></span>目录，系统管理命令通常位于<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/sbin</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/usr/sbin</code></span>目录，一般用户可以执行用户命令，而执行系统管理命令经常需要<span style="overflow-x: auto; max-width:100%; display:inline;"><code>root</code></span>权限。</p>
<p>系统调用和库函数的区别将在 <a href="http://akaedu.github.io/book/ch19s02.html#asmc.main">第 2 节 “<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span>函数和启动例程”</a> 说明。</p>
<p>Man Page 中有些页面有重名，比如敲 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>man printf</code></span> 命令看到的并不是 C 函数 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>printf</code></span>，而是位于第 1 个 Section的系统命令 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>printf</code></span>，要查看位于第 3 个 Section 的 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>printf</code></span> 函数应该敲 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>man 3 printf</code></span>，也可以敲 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>man -k printf</code></span> 命令搜索哪些页面的主题包含 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>printf</code></span> 关键字。</p>
<h2 id="gdb">二、GDB 调试</h2>
<blockquote class="content-quote">
<p>http://akaedu.github.io/book/ch10s01.html &ldquo;gdb&rdquo;<br />
http://akaedu.github.io/book/ch10s02.html &ldquo;断点&rdquo;<br />
http://akaedu.github.io/book/ch10s03.html &ldquo;观察点&rdquo;</p>
</blockquote>
<p><strong>表 10.1. gdb基本命令1</strong></p>
<div class="pure-table-scrollable"><table class="pure-table pure-table-horizontal">
<thead>
<tr>
<th>命令</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>backtrace（或bt）</td>
<td>查看各级函数调用及参数</td>
</tr>
<tr>
<td>finish</td>
<td>连续运行到当前函数返回为止，然后停下来等待命令</td>
</tr>
<tr>
<td>frame（或f） 帧编号</td>
<td>选择栈帧</td>
</tr>
<tr>
<td>info（或i） locals</td>
<td>查看当前栈帧局部变量的值</td>
</tr>
<tr>
<td>list（或l）</td>
<td>列出源代码，接着上次的位置往下列，每次列10行</td>
</tr>
<tr>
<td>list 行号</td>
<td>列出从第几行开始的源代码</td>
</tr>
<tr>
<td>list 函数名</td>
<td>列出某个函数的源代码</td>
</tr>
<tr>
<td>next（或n）</td>
<td>执行下一行语句</td>
</tr>
<tr>
<td>print（或p）</td>
<td>打印表达式的值，通过表达式可以修改变量的值或者调用函数</td>
</tr>
<tr>
<td>quit（或q）</td>
<td>退出<span style="overflow-x: auto; max-width:100%; display:inline;"><code>gdb</code></span>调试环境</td>
</tr>
<tr>
<td>set var</td>
<td>修改变量的值</td>
</tr>
<tr>
<td>start</td>
<td>开始执行程序，停在<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span>函数第一行语句前面等待命令</td>
</tr>
<tr>
<td>step（或s）</td>
<td>执行下一行语句，如果有函数调用则进入到函数中</td>
</tr>
</tbody>
</table></div>
<p><strong>表 10.2. gdb基本命令2</strong></p>
<div class="pure-table-scrollable"><table class="pure-table pure-table-horizontal">
<thead>
<tr>
<th>命令</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>break（或b） 行号</td>
<td>在某一行设置断点</td>
</tr>
<tr>
<td>break 函数名</td>
<td>在某个函数开头设置断点</td>
</tr>
<tr>
<td>break &hellip; if &hellip;</td>
<td>设置条件断点</td>
</tr>
<tr>
<td>continue（或c）</td>
<td>从当前位置开始连续运行程序</td>
</tr>
<tr>
<td>delete breakpoints 断点号</td>
<td>删除断点</td>
</tr>
<tr>
<td>display 变量名</td>
<td>跟踪查看某个变量，每次停下来都显示它的值</td>
</tr>
<tr>
<td>disable breakpoints 断点号</td>
<td>禁用断点</td>
</tr>
<tr>
<td>enable 断点号</td>
<td>启用断点</td>
</tr>
<tr>
<td>info（或i） breakpoints</td>
<td>查看当前设置了哪些断点</td>
</tr>
<tr>
<td>run（或r）</td>
<td>从头开始连续运行程序</td>
</tr>
<tr>
<td>undisplay 跟踪显示号</td>
<td>取消跟踪显示</td>
</tr>
</tbody>
</table></div>
<p><strong>表 10.3. gdb基本命令3</strong></p>
<div class="pure-table-scrollable"><table class="pure-table pure-table-horizontal">
<thead>
<tr>
<th>命令</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>watch</td>
<td>设置观察点</td>
</tr>
<tr>
<td>info（或i） watchpoints</td>
<td>查看当前设置了哪些观察点</td>
</tr>
<tr>
<td>x</td>
<td>从某个位置开始打印存储单元的内容，全部当成字节来看，而不区分哪个字节属于哪个变量</td>
</tr>
</tbody>
</table></div>
<h2 id="_1">三、变量的存储布局</h2>
<p><img class="pure-img" alt="123123123" src="https://zromyk.gitee.io/myblog-figurebed/post/Linux C.assets/123123123.png" /></p>
<ul>
<li>.text 段：在内存中被映射为只读，程序的二进制代码存放于此。</li>
<li>.data 段<ul>
<li>堆区（heap） ：一般由程序员分配释放，若程序员不释放，程序结束时可能由OS回收。注意它与数据结构中的堆是两回事，分配方式倒是类似于链表。</li>
<li>栈区（stack）： 由编译器自动分配释放 ，存放函数的参数值，局部变量的值等。其操作方式类似于数据结构中的栈。</li>
<li>全局区（静态区 static）：全局变量和静态变量的存储是放在一块的。程序结束后有系统释放。<strong>初始化</strong> 的全局变量和静态变量在 Static Data。</li>
</ul>
</li>
<li>.bss 段：未初始化的全局变量和未初始化的静态变量（.bss 段在程序开始后会自动清零）。</li>
<li>文字常量区：程序中使用的常量存储在此区域。程序结束后由系统释放。</li>
<li>程序代码区：存放函数体的二进制代码。运行程序就是执行代码，代码要执行就要加载进内存。</li>
</ul>
<p>https://leetcode-cn.com/leetbook/read/cpp-interview-highlights/e4vkxv/</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c++">#include &lt;iostream&gt;
using namespace std;

/*
说明：C++ 中不再区分初始化和未初始化的全局变量、静态变量的存储区，如果非要区分下述程序标注在了括号中
*/

int g_var = 0; // g_var 在全局区（.data 段）
char *gp_var;  // gp_var 在全局区（.bss 段）

int main()
{
    int var;                    // var 在栈区
    char *p_var;                // p_var 在栈区
    char arr[] = &quot;abc&quot;;         // arr 为数组变量，存储在栈区；&quot;abc&quot;为字符串常量，存储在常量区
    char *p_var1 = &quot;123456&quot;;    // p_var1 在栈区；&quot;123456&quot;为字符串常量，存储在常量区
    static int s_var = 0;       // s_var 为静态变量，存在静态存储区（.data 段）
    p_var = (char *)malloc(10); // 分配得来的 10 个字节的区域在堆区
    free(p_var);
    return 0;
}
</code></span></code></pre>
<p>http://akaedu.github.io/book/ch19s03.html   &ldquo;变量的存储布局&rdquo;</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;stdio.h&gt;

const int A = 10;
int a = 20;
static int b = 30;
int c;

int main(void)
{
    static int a = 40;
    char b[] = &quot;Hello world&quot;;
    register int c = 50;

    printf(&quot;Hello world %d\n&quot;, c);

    return 0;
}
</code></span></code></pre>
<p><strong>作用域（Scope）</strong>这个概念适用于所有标识符，而不仅仅是变量，C语言的作用域分为以下几类：</p>
<ul>
<li>函数作用域（Function Scope），标识符在整个函数中都有效。只有语句标号属于函数作用域。标号在函数中不需要先声明后使用，在前面用一个<span style="overflow-x: auto; max-width:100%; display:inline;"><code>goto</code></span>语句也可以跳转到后面的某个标号，但仅限于同一个函数之中。</li>
<li>文件作用域（File Scope），标识符从它声明的位置开始直到这个程序文件[<a href="http://akaedu.github.io/book/ch19s03.html#ftn.id2778429">30</a>]的末尾都有效。例如上例中<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span>函数外面的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>A</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>b</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>c</code></span>，还有<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span>也算，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>printf</code></span>其实是在<span style="overflow-x: auto; max-width:100%; display:inline;"><code>stdio.h</code></span>中声明的，被包含到这个程序文件中了，所以也算文件作用域的。</li>
<li>块作用域（Block Scope），标识符位于一对{}括号中（函数体或语句块），从它声明的位置开始到右}括号之间有效。例如上例中<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span>函数里的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>b</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>c</code></span>。此外，函数定义中的形参也算块作用域的，从声明的位置开始到函数末尾之间有效。</li>
<li>函数原型作用域（Function Prototype Scope），标识符出现在函数原型中，这个函数原型只是一个声明而不是定义（没有函数体），那么标识符从声明的位置开始到在这个原型末尾之间有效。例如<span style="overflow-x: auto; max-width:100%; display:inline;"><code>int foo(int a, int b);</code></span>中的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>b</code></span>。</li>
</ul>
<p>对属于同一 <strong>命名空间（Name Space）</strong>的重名标识符，内层作用域的标识符将覆盖外层作用域的标识符，例如局部变量名在它的函数中将覆盖重名的全局变量。命名空间可分为以下几类：</p>
<ul>
<li>语句标号单独属于一个命名空间。例如在函数中局部变量和语句标号可以重名，互不影响。由于使用标号的语法和使用其它标识符的语法都不一样，编译器不会把它和别的标识符弄混。</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>struct</code></span>，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>enum</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>union</code></span>（下一节介绍<span style="overflow-x: auto; max-width:100%; display:inline;"><code>union</code></span>）的类型Tag属于一个命名空间。由于Tag前面总是带<span style="overflow-x: auto; max-width:100%; display:inline;"><code>struct</code></span>，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>enum</code></span>或<span style="overflow-x: auto; max-width:100%; display:inline;"><code>union</code></span>关键字，所以编译器不会把它和别的标识符弄混。</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>struct</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>union</code></span>的成员名属于一个命名空间。由于成员名总是通过<span style="overflow-x: auto; max-width:100%; display:inline;"><code>.</code></span>或<span style="overflow-x: auto; max-width:100%; display:inline;"><code>-&gt;</code></span>运算符来访问而不会单独使用，所以编译器不会把它和别的标识符弄混。</li>
<li>所有其它标识符，例如变量名、函数名、宏定义、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>typedef</code></span>的类型名、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>enum</code></span>成员等等都属于同一个命名空间。如果有重名的话，宏定义覆盖所有其它标识符，因为它在预处理阶段而不是编译阶段处理，除了宏定义之外其它几类标识符按上面所说的规则处理，内层作用域覆盖外层作用域。</li>
</ul>
<p><strong>标识符的链接属性（Linkage）</strong>有三种：</p>
<ul>
<li>外部链接（External Linkage），如果最终的可执行文件由多个程序文件链接而成，一个标识符在任意程序文件中即使声明多次也都代表同一个变量或函数，则这个标识符具有External Linkage。具有External Linkage的标识符编译后在符号表中是<span style="overflow-x: auto; max-width:100%; display:inline;"><code>GLOBAL</code></span>的符号。例如上例中<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span>函数外面的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>c</code></span>，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>printf</code></span>也算。</li>
<li>内部链接（Internal Linkage），如果一个标识符在某个程序文件中即使声明多次也都代表同一个变量或函数，则这个标识符具有Internal Linkage。例如上例中<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span>函数外面的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>b</code></span>。如果有另一个<span style="overflow-x: auto; max-width:100%; display:inline;"><code>foo.c</code></span>程序和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main.c</code></span>链接在一起，在<span style="overflow-x: auto; max-width:100%; display:inline;"><code>foo.c</code></span>中也声明一个<span style="overflow-x: auto; max-width:100%; display:inline;"><code>static int b;</code></span>，则那个<span style="overflow-x: auto; max-width:100%; display:inline;"><code>b</code></span>和这个<span style="overflow-x: auto; max-width:100%; display:inline;"><code>b</code></span>不代表同一个变量。具有Internal Linkage的标识符编译后在符号表中是<span style="overflow-x: auto; max-width:100%; display:inline;"><code>LOCAL</code></span>的符号，但<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span>函数里面那个<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a</code></span>不能算Internal Linkage的，因为即使在同一个程序文件中，在不同的函数中声明多次，也不代表同一个变量。</li>
<li>无链接（No Linkage）。除以上情况之外的标识符都属于No Linkage的，例如函数的局部变量，以及不表示变量和函数的其它标识符。</li>
</ul>
<p><strong>存储类修饰符（Storage Class Specifier）</strong>有以下几种关键字，可以修饰变量或函数声明：</p>
<ul>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>static</code></span>，用它修饰的变量的存储空间是静态分配的，用它修饰的文件作用域的变量或函数具有Internal Linkage。</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>auto</code></span>，用它修饰的变量在函数调用时自动在栈上分配存储空间，函数返回时自动释放，例如上例中<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span>函数里的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>b</code></span>其实就是用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>auto</code></span>修饰的，只不过<span style="overflow-x: auto; max-width:100%; display:inline;"><code>auto</code></span>可以省略不写，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>auto</code></span>不能修饰文件作用域的变量。</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>register</code></span>，编译器对于用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>register</code></span>修饰的变量会尽可能分配一个专门的寄存器来存储，但如果实在分配不开寄存器，编译器就把它当<span style="overflow-x: auto; max-width:100%; display:inline;"><code>auto</code></span>变量处理了，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>register</code></span>不能修饰文件作用域的变量。现在一般编译器的优化都做得很好了，它自己会想办法有效地利用CPU的寄存器，所以现在<span style="overflow-x: auto; max-width:100%; display:inline;"><code>register</code></span>关键字也用得比较少了。</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>extern</code></span>，上面讲过，链接属性是根据一个标识符多次声明时是不是代表同一个变量或函数来分类的，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>extern</code></span>关键字就用于多次声明同一个标识符，下一章再详细介绍它的用法。</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>typedef</code></span>，在<a href="http://akaedu.github.io/book/ch16s02.html#op.sizeoftypedef">第 2.4 节 “sizeof运算符与typedef类型声明”</a>讲过这个关键字，它并不是用来修饰变量的，而是定义一个类型名。在那一节也讲过，看<span style="overflow-x: auto; max-width:100%; display:inline;"><code>typedef</code></span>声明怎么看呢，首先去掉<span style="overflow-x: auto; max-width:100%; display:inline;"><code>typedef</code></span>把它看成变量声明，看这个变量是什么类型的，那么<span style="overflow-x: auto; max-width:100%; display:inline;"><code>typedef</code></span>就定义了一个什么类型，也就是说，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>typedef</code></span>在语法结构中出现的位置和前面几个关键字一样，也是修饰变量声明的，所以从语法（而不是语义）的角度把它和前面几个关键字归类到一起。</li>
</ul>
<p>注意，上面介绍的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>const</code></span>关键字不是一个Storage Class Specifier，虽然看起来它也修饰一个变量声明，但是在以后介绍的更复杂的声明中<span style="overflow-x: auto; max-width:100%; display:inline;"><code>const</code></span>在语法结构中允许出现的位置和Storage Class Specifier是不完全相同的。<span style="overflow-x: auto; max-width:100%; display:inline;"><code>const</code></span>和以后要介绍的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>restrict</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>volatile</code></span>关键字属于同一类语法元素，称为类型限定符（Type Qualifier）。</p>
<p><strong>变量的生存期（Storage Duration，或者Lifetime）</strong>分为以下几类：</p>
<ul>
<li>静态生存期（Static Storage Duration），具有外部或内部链接属性，或者被<span style="overflow-x: auto; max-width:100%; display:inline;"><code>static</code></span>修饰的变量，在程序开始执行时分配和初始化一次，此后便一直存在直到程序结束。这种变量通常位于<span style="overflow-x: auto; max-width:100%; display:inline;"><code>.rodata</code></span>，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>.data</code></span>或<span style="overflow-x: auto; max-width:100%; display:inline;"><code>.bss</code></span>段，例如上例中<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span>函数外的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>A</code></span>，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a</code></span>，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>b</code></span>，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>c</code></span>，以及<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span>函数里的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a</code></span>。</li>
<li>自动生存期（Automatic Storage Duration），链接属性为无链接并且没有被<span style="overflow-x: auto; max-width:100%; display:inline;"><code>static</code></span>修饰的变量，这种变量在进入块作用域时在栈上或寄存器中分配，在退出块作用域时释放。例如上例中<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span>函数里的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>b</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>c</code></span>。</li>
<li>动态分配生存期（Allocated Storage Duration），以后会讲到调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>malloc</code></span>函数在进程的堆空间中分配内存，调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>free</code></span>函数可以释放这种存储空间。</li>
</ul>
<h2 id="c">四、C内联汇编</h2>
<p>如果需要执行多条汇编指令，则应该用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>\n\t</code></span>将各条指令分隔开，例如：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>__asm__(&quot;movl $1, %eax\n\t&quot;
    &quot;movl $4, %ebx\n\t&quot;
    &quot;int $0x80&quot;);
</code></span></code></pre>
<p>通常 C 代码中的内联汇编需要和C的变量建立关联，需要用到完整的内联汇编格式：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>__asm__(assembler template 
    : output operands                  /* optional */
    : input operands                   /* optional */
    : list of clobbered registers      /* optional */
    );
</code></span></code></pre>
<p>这种格式由四部分组成，第一部分是汇编指令，和上面的例子一样，第二部分和第三部分是约束条件，第二部分指示汇编指令的运算结果要输出到哪些C操作数中，C操作数应该是左值表达式，第三部分指示汇编指令需要从哪些C操作数获得输入，第四部分是在汇编指令中被修改过的寄存器列表，指示编译器哪些寄存器的值在执行这条<span style="overflow-x: auto; max-width:100%; display:inline;"><code>__asm__</code></span>语句时会改变。后三个部分都是可选的，如果有就填写，没有就空着只写个<span style="overflow-x: auto; max-width:100%; display:inline;"><code>:</code></span>号。</p>
<p><strong>例 19.6. 内联汇编</strong></p>
<p>http://akaedu.github.io/book/ch19s05.html   &ldquo;例 19.6. 内联汇编&rdquo;</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>#include &lt;stdio.h&gt;

int main() 
{
        int a = 10, b;

    __asm__(&quot;movl %1, %%eax\n\t&quot;
        &quot;movl %%eax, %0\n\t&quot;
        :&quot;=r&quot;(b)        /* output */
        :&quot;r&quot;(a)         /* input */
        :&quot;%eax&quot;         /* clobbered register */
        );
    printf(&quot;Result: %d, %d\n&quot;, a, b);
    return 0;
}
</code></span></code></pre>
<p>这个程序将变量<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a</code></span>的值赋给<span style="overflow-x: auto; max-width:100%; display:inline;"><code>b</code></span>。<span style="overflow-x: auto; max-width:100%; display:inline;"><code>"r"(a)</code></span>指示编译器分配一个寄存器保存变量<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a</code></span>的值，作为汇编指令的输入，也就是指令中的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>%1</code></span>（按照约束条件的顺序，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>b</code></span>对应<span style="overflow-x: auto; max-width:100%; display:inline;"><code>%0</code></span>，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a</code></span>对应<span style="overflow-x: auto; max-width:100%; display:inline;"><code>1%</code></span>），至于<span style="overflow-x: auto; max-width:100%; display:inline;"><code>%1</code></span>究竟代表哪个寄存器则由编译器自己决定。汇编指令首先把<span style="overflow-x: auto; max-width:100%; display:inline;"><code>%1</code></span>所代表的寄存器的值传给<span style="overflow-x: auto; max-width:100%; display:inline;"><code>eax</code></span>（为了和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>%1</code></span>这种占位符区分，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>eax</code></span>前面要求加两个<span style="overflow-x: auto; max-width:100%; display:inline;"><code>%</code></span>号），然后把<span style="overflow-x: auto; max-width:100%; display:inline;"><code>eax</code></span>的值再传给<span style="overflow-x: auto; max-width:100%; display:inline;"><code>%0</code></span>所代表的寄存器。<span style="overflow-x: auto; max-width:100%; display:inline;"><code>"=r"(b)</code></span>就表示把<span style="overflow-x: auto; max-width:100%; display:inline;"><code>%0</code></span>所代表的寄存器的值输出给变量<span style="overflow-x: auto; max-width:100%; display:inline;"><code>b</code></span>。在执行这两条指令的过程中，寄存器<span style="overflow-x: auto; max-width:100%; display:inline;"><code>eax</code></span>的值被改变了，所以把<span style="overflow-x: auto; max-width:100%; display:inline;"><code>"%eax"</code></span>写在第四部分，告诉编译器在执行这条<span style="overflow-x: auto; max-width:100%; display:inline;"><code>__asm__</code></span>语句时<span style="overflow-x: auto; max-width:100%; display:inline;"><code>eax</code></span>要被改写，所以在此期间不要用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>eax</code></span>保存其它值。</p>
<p>我们看一下这个程序的反汇编结果：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>       __asm__(&quot;movl %1, %%eax\n\t&quot;
 80483dc:       8b 55 f8                mov    -0x8(%ebp),%edx
 80483df:       89 d0                   mov    %edx,%eax
 80483e1:       89 c2                   mov    %eax,%edx
 80483e3:       89 55 f4                mov    %edx,-0xc(%ebp)
                &quot;movl %%eax, %0\n\t&quot;
                :&quot;=r&quot;(b)        /* output */
                :&quot;r&quot;(a)         /* input */
                :&quot;%eax&quot;         /* clobbered register */
                );
</code></span></code></pre>
<h2 id="_2">五、多目标文件的链接</h2>
<p>http://akaedu.github.io/book/ch20s01.html   &ldquo;多目标文件的链接&rdquo;</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">gcc main.c stack.c -o main
</code></span></code></pre>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">gcc -c main.c
$ gcc -c stack.c
$ gcc main.o stack.o -o main
</code></span></code></pre>
<p><strong>图 20.1. 多目标文件的链接</strong></p>
<p><img class="pure-img" alt="多目标文件的链接" src="https://zromyk.gitee.io/myblog-figurebed/post/Linux C.assets/link.multiobj.png" /></p>
<p>为什么在可执行文件<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span>的每个段中来自<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main.o</code></span>的变量或函数都在前面，而来自<span style="overflow-x: auto; max-width:100%; display:inline;"><code>stack.o</code></span>的变量或函数都在后面呢？我们可以试试把<span style="overflow-x: auto; max-width:100%; display:inline;"><code>gcc</code></span>命令中的两个目标文件反过来写：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ gcc stack.o main.o -o main
</code></span></code></pre>
<p>结果正如我们所预料的，可执行文件<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span>的每个段中来自<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main.o</code></span>的变量或函数都排到后面了。实际上链接的过程是由一个链接脚本（Linker Script）控制的，链接脚本决定了给每个段分配什么地址，如何对齐，哪个段在前，哪个段在后，哪些段合并到同一个Segment，另外链接脚本还要插入一些符号到最终生成的文件中，例如<span style="overflow-x: auto; max-width:100%; display:inline;"><code>__bss_start</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>_edata</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>_end</code></span>等。</p>
<p>如果用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>ld</code></span>做链接时没有用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>-T</code></span>选项指定链接脚本，则使用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>ld</code></span>的默认链接脚本，默认链接脚本可以用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>ld --verbose</code></span>命令查看（由于比较长，只列出一些片断）：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ ld --verbose
...
using internal linker script:
==================================================
/* Script for -z combreloc: combine and sort reloc sections */
OUTPUT_FORMAT(&quot;elf32-i386&quot;, &quot;elf32-i386&quot;,
          &quot;elf32-i386&quot;)
OUTPUT_ARCH(i386)
# ENTRY(_start) 说明 _start 是整个程序的入口点。
# 因此 _start 是入口点并不是规定死的，是可以改用其它函数做入口点的。
ENTRY(_start) 
...
SECTIONS
{
  /* Read-only sections, merged into text segment: */
  # 下面的语句是 Text Segment 的起始地址
  # 这个Segment包含后面列出的那些段，.plt、.text、.rodata 等等
  # 每个段的描述格式都是“段名 : { 组成 }”
  # 例如 .plt : { *(.plt) }，左边表示最终生成的文件的 .plt 段，右边表示所有目标文件的 .plt 段
  # 意思是最终生成的文件的.plt段由各目标文件的.plt段组成。
  PROVIDE (__executable_start = 0x08048000); . = 0x08048000 + SIZEOF_HEADERS;
  .interp         : { *(.interp) }
  .note.gnu.build-id : { *(.note.gnu.build-id) }
  .hash           : { *(.hash) }
  .gnu.hash       : { *(.gnu.hash) }
  .dynsym         : { *(.dynsym) }
  .dynstr         : { *(.dynstr) }
  .gnu.version    : { *(.gnu.version) }
  .gnu.version_d  : { *(.gnu.version_d) }
  .gnu.version_r  : { *(.gnu.version_r) }
  .rel.dyn        :
...
  .rel.plt        : { *(.rel.plt) }
...
  .init           :
...
  .plt            : { *(.plt) }
  .text           :
...
  .fini           :
...
  .rodata         : { *(.rodata .rodata.* .gnu.linkonce.r.*) }
...
  .eh_frame       : ONLY_IF_RO { KEEP (*(.eh_frame)) }
...
  /* Adjust the address for the data segment.  We want to adjust up to
     the same address within the page on the next page up.  */
  # 下面的语句是 Data Segment 的起始地址，要做一系列的对齐操作
  # 这个 Segment 包含后面列出的那些段，.got、.data、.bss 等等。
  . = ALIGN (CONSTANT (MAXPAGESIZE)) - ((CONSTANT (MAXPAGESIZE) - .) &amp; (CONSTANT (MAXPAGESIZE) - 1)); . = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE));
...
  .ctors          :
...
  .dtors          :
...
  .jcr            : { KEEP (*(.jcr)) }
...
  .dynamic        : { *(.dynamic) }
  .got            : { *(.got) }
...
  .got.plt        : { *(.got.plt) }
  .data           :
...
  _edata = .; PROVIDE (edata = .);
  __bss_start = .;
  .bss            :
...
  _end = .; PROVIDE (end = .);
  . = DATA_SEGMENT_END (.);
  /* Stabs debugging sections.  */
...
  /* DWARF debug sections.
     Symbols in the DWARF debugging sections are relative to the beginning
     of the section so we begin them at 0.  */
...
}

==================================================
</code></span></code></pre>
<p>Data Segment 的后面还有其它一些 Segment，主要是调试信息。</p>
<h3 id="51">5.1 定义和声明</h3>
<h4 id="externstatic"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>extern</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>static</code></span>关键字</h4>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ gcc -c main.c -Wall # 该选项能发现程序中一系列的常见错误警告.

main.c: In function ‘main’:
main.c:8: warning: implicit declaration of function ‘push’
main.c:12: warning: implicit declaration of function ‘is_empty’
main.c:13: warning: implicit declaration of function ‘pop’
</code></span></code></pre>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>gcc -Wall test.c -o test</code></span></p>
<p>该选项相当于同时使用了下列所有的选项：</p>
<div class="pure-table-scrollable"><table class="pure-table pure-table-horizontal">
<tr><td> unused-function </td><td> 遇到仅声明过但尚未定义的静态函数时发出警告。</td></tr>
<tr><td> unused-label </td><td> 遇到声明过但不使用的标号的警告。</td></tr>
<tr><td> unused-parameter </td><td> 从未用过的函数参数的警告。</td></tr>
<tr><td> unused-variable </td><td> 在本地声明但从未用过的变量的警告。</td></tr>
<tr><td> unused-value </td><td> 仅计算但从未用过的值得警告。</td></tr>
<tr><td> Format </td><td> 检查对printf和scanf等函数的调用，确认各个参数类型和格式串中的一致。</td></tr>
<tr><td> implicit-int </td><td> 警告没有规定类型的声明。</td></tr>
<tr><td> implicit-function- </td><td> 在函数在未经声明就使用时给予警告。</td></tr>
<tr><td> char-subscripts </td><td> 警告把char类型作为数组下标。这是常见错误，程序员经常忘记在某些机器上char有符号。</td></tr>
<tr><td> missing-braces </td><td> 聚合初始化两边缺少大括号。</td></tr>
<tr><td> Parentdeses </td><td> 在某些情况下如果忽略了括号，编译器就发出警告。</td></tr>
<tr><td> return-type </td><td> 如果函数定义了返回类型，而默认类型是int型，编译器就发出警告。同时警告那些不带返回值的 return语句，如果他们所属的函数并非void类型。</td></tr>
<tr><td> sequence-point </td><td> 出现可疑的代码元素时，发出报警。</td></tr>
<tr><td> Switch </td><td> 如果某条switch语句的参数属于枚举类型，但是没有对应的case语句使用枚举元素，编译器就发出警告（在switch语句中使用default分支能够防止这个警告）。超出枚举范围的case语句同样会导致这个警告。</td></tr>
<tr><td> strict-aliasing </td><td> 对变量别名进行最严格的检查。</td></tr>
<tr><td> unknown-pragmas </td><td> 使用了不允许的#pragma。</td></tr>
<tr><td> Uninitialized </td><td> 在初始化之前就使用自动变量。</td></tr>
</table></div>

<p>下面是使用 -Wall 选项的时候没有生效的一些警告项：</p>
<div class="pure-table-scrollable"><table class="pure-table pure-table-horizontal">
<tr><td> cast-align </td><td> 一旦某个指针类型强制转换时，会导致目标所需的地址对齐边界扩展，编译器就发出警告。例如，某些机器上只能在2或4字节边界上访问整数，如果在这种机型上把char *强制转换成int *类型， 编译器就发出警告。</td></tr>
<tr><td> sign-compare </td><td> 将有符号类型和无符号类型数据进行比较时发出警告。</td></tr>
<tr><td> missing-prototypes  </td><td> 如果没有预先声明函数原形就定义了全局函数，编译器就发出警告。即使函数定义自身提供了函数原形也会产生这个警告。这样做的目的是检查没有在头文件中声明的全局函数。</td></tr>
<tr><td> Packed </td><td> 当结构体带有packed属性但实际并没有出现紧缩式给出警告。</td></tr>
<tr><td> Padded </td><td> 如果结构体通过充填进行对齐则给出警告。</td></tr>
<tr><td> unreachable-code </td><td> 如果发现从未执行的代码时给出警告。</td></tr>
<tr><td> Inline </td><td> 如果某函数不能内嵌（inline），无论是声明为inline或者是指定了-finline-functions 选项，编译器都将发出警告。</td></tr>
<tr><td> disabled-optimization </td><td> 当需要太长时间或过多资源而导致不能完成某项优化时给出警告。</td></tr>
</table></div>

<p>上面是使用 -Wall 选项时没有生效，但又比较常用的一些警告选项。<br />
本文中要介绍的最后一个常用警告选项是 -Werror。<br />
使用该选项后，GCC 发现可疑之处时不会简单的发出警告就算完事，而是将警告作为一个错误而中断编译过程。<br />
该选项在希望得到高质量代码时非常有用。</p>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>extern</code></span> 关键字表示这个标识符具有 External Linkage。</p>
<p>函数声明中的 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>extern</code></span> 也可以省略不写，不写 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>extern</code></span> 的函数声明也表示这个函数具有 External Linkage。</p>
<p>在如下工程中：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ tree
.
|-- main.c
|-- stack.c
`-- stack.h

0 directories, 3 files
</code></span></code></pre>
<p>用 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>gcc -c main.c</code></span> 编译，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>gcc</code></span> 会自动在 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>main.c</code></span> 所在的目录中找到 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>stack.h</code></span>。</p>
<p>假如把<span style="overflow-x: auto; max-width:100%; display:inline;"><code>stack.h</code></span>移到一个子目录下：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ tree
.
|-- main.c
`-- stack
    |-- stack.c
    `-- stack.h

1 directory, 3 files
</code></span></code></pre>
<p>则需要用 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>gcc -c main.c -I stack</code></span> 编译。用 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>-I</code></span> 选项告诉 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>gcc</code></span> 头文件要到子目录 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>stack</code></span> 里找。</p>
<p>在 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>#include</code></span> 预处理指示中可以使用相对路径，例如把上面的代码改成 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>#include "stack/stack.h"</code></span>，那么编译时就不需要加 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>-Istack</code></span> 选项了，因为 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>gcc</code></span> 会自动在 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>main.c</code></span> 所在的目录中查找，而头文件相对于 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>main.c</code></span> 所在目录的相对路径正是 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>stack/stack.h</code></span>。</p>
<p><strong>表 20.1. Storage Class 关键字对函数声明的作用</strong></p>
<div class="pure-table-scrollable"><table class="pure-table pure-table-horizontal">
<thead>
<tr>
<th>Storage Class</th>
<th>File Scope Declaration</th>
<th>Block Scope Declaration</th>
</tr>
</thead>
<tbody>
<tr>
<td>none</td>
<td>previous linkage can define<br />在文件作用域允许定义函数</td>
<td>previous linkage cannot define<br />在块作用域不允许定义函数</td>
</tr>
<tr>
<td>extern</td>
<td>previous linkage can define<br />在文件作用域允许定义函数</td>
<td>previous linkage cannot define<br />在块作用域不允许定义函数</td>
</tr>
<tr>
<td>static</td>
<td>internal linkage can define</td>
<td>N/A<br />在块作用域中不允许用 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>static</code></span>关键字声明函数</td>
</tr>
</tbody>
</table></div>
<p>以前我们说 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>extern</code></span> 关键字表示这个标识符具有 External Linkage 其实是不准确的，准确地说应该是 Previous Linkage。</p>
<p><strong>Previous Linkage 的定义</strong>是：这次声明的标识符具有什么样的 Linkage 取决于前一次声明，这前一次声明具有相同的标识符名，而且必须是文件作用域的声明，如果在程序文件中找不到前一次声明（这次声明是第一次声明），那么这个标识符具有 External Linkage 。例如在一个程序文件中在文件作用域两次声明同一个函数：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">static int f(void); /* internal linkage */
extern int f(void); /* previous linkage */
</code></span></code></pre>
<p>则这里的 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>extern</code></span> 修饰的标识符具有 Interanl Linkage 而不是 External Linkage。</p>
<p>关于变量声明的规则要复杂一些：</p>
<p><strong>表 20.2. Storage Class关键字对变量声明的作用</strong></p>
<div class="pure-table-scrollable"><table class="pure-table pure-table-horizontal">
<thead>
<tr>
<th>Storage Class</th>
<th>File Scope Declaration</th>
<th>Block Scope Declaration</th>
</tr>
</thead>
<tbody>
<tr>
<td>none</td>
<td>external linkage<br />static duration<br />static initializer<br />tentative definition</td>
<td>no linkage<br />automatic duration<br />dynamic initializer<br />definition</td>
</tr>
<tr>
<td>extern</td>
<td>previous linkage<br />static duration<br />no initializer[*]<br />not a definition</td>
<td>previous linkage<br />static duration<br />no initializer<br />not a definition</td>
</tr>
<tr>
<td>static</td>
<td>internal linkage<br />static duration<br />static initializer<br />tentative definition</td>
<td>no linkage<br />static duration<br />static initializer<br />definition</td>
</tr>
</tbody>
</table></div>
<p>上表的每个单元格里分成四行，分别描述变量的 <strong>链接属性</strong>、<strong>生存期</strong>，<strong>以及这种变量如何初始化</strong>，<strong>是否算变量定义</strong>。</p>
<ul>
<li>链接属性<ul>
<li>External Linkage</li>
<li>Internal Linkage</li>
<li>No Linkage</li>
<li>Previous Linkage</li>
</ul>
</li>
<li>
<p>生存期</p>
<ul>
<li>Static Duration</li>
<li>Automatic Duration</li>
</ul>
</li>
<li>
<p>初始化</p>
<ul>
<li>Static Initializer（表示Initializer中只能使用常量表达式，达式的值必须在编译时就能确定）</li>
<li>Dynamic Initializer（表示Initializer中可以使用任意的右值表达式，表达式的值可以在运行时计算）</li>
</ul>
</li>
<li>是否算变量定义有三种情况<ul>
<li>Definition（算变量定义）</li>
<li>Not a Definition（不算变量定义）</li>
<li>Tentative Definition（暂定的变量定义）</li>
</ul>
</li>
</ul>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">int i1 = 1;         // definition, external linkage
static int i2 = 2;  // definition, internal linkage
extern int i3 = 3;  // definition, external linkage
int i4;             // tentative definition, external linkage
static int i5;      // tentative definition, internal linkage
int i1;             // valid tentative definition, refers to previous
int i2;             // 6.2.2 renders undefined, linkage disagreement
int i3;             // valid tentative definition, refers to previous
int i4;             // valid tentative definition, refers to previous
int i5;             // 6.2.2 renders undefined, linkage disagreement
extern int i1;      // refers to previous, whose linkage is external
extern int i2;      // refers to previous, whose linkage is internal
extern int i3;      // refers to previous, whose linkage is external
extern int i4;      // refers to previous, whose linkage is external
extern int i5;      // refers to previous, whose linkage is internal
</code></span></code></pre>
<p>变量 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>i2</code></span> 和 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>i5</code></span> 第一次声明为 Internal Linkage，第二次又声明为 External Linkage，这是不允许的，编译器会报错。</p>
<p>注意上表中标有 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>[*]</code></span> 的单元格，对于文件作用域的 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>extern</code></span> 变量声明，C99 是允许带 Initializer 的，并且认为它是一个定义，但是 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>gcc</code></span> 对于这种写法会报警告，为了兼容性应避免这种写法。</p>
<h3 id="52">5.2 静态库</h3>
<p>假如当前目录结构是：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ tree
.
|-- main.c
`-- stack
    |-- is_empty.c
    |-- pop.c
    |-- push.c
    |-- stack.c
    `-- stack.h

1 directory, 6 files
</code></span></code></pre>
<p>我们把 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>stack.c</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>push.c</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>pop.c</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>is_empty.c</code></span> 编译成目标文件：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ gcc -c stack/stack.c stack/push.c stack/pop.c stack/is_empty.c
</code></span></code></pre>
<p>然后打包成一个静态库 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>libstack.a</code></span></p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ ar rs libstack.a stack.o push.o pop.o is_empty.o
ar: creating libstack.a
</code></span></code></pre>
<p>库文件名都是以 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>lib</code></span> 开头的，静态库以<span style="overflow-x: auto; max-width:100%; display:inline;"><code>.a</code></span>作为后缀，表示 Archive。</p>
<ul>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>ar</code></span>命令类似于<span style="overflow-x: auto; max-width:100%; display:inline;"><code>tar</code></span>命令，起一个打包的作用，但是把目标文件打包成静态库只能用 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>ar</code></span> 命令而不能用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>tar</code></span>命令。</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>r</code></span> 表示将后面的文件列表添加到文件包，如果文件包不存在就创建它，如果文件包中已有同名文件就替换成新的。</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>s</code></span> 是专用于生成静态库的，表示为静态库创建索引，这个索引被链接器使用。</li>
</ul>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>ranlib</code></span>命令也可以为静态库创建索引，以上命令等价于：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ ar r libstack.a stack.o push.o pop.o is_empty.o
$ ranlib libstack.a
</code></span></code></pre>
<p>然后我们把 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>libstack.a</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main.c</code></span> 编译链接在一起：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ gcc main.c -L. -l stack -I stack -o main
</code></span></code></pre>
<ul>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>-L</code></span> 选项告诉编译器去哪里找需要的库文件。</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>-L.</code></span> 表示在当前目录找。</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>-l stack</code></span> 告诉编译器要链接 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>libstack</code></span>库。</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>-I</code></span> 选项告诉编译器去哪里找头文件。</li>
</ul>
<p>注意，即使库文件就在当前目录，编译器默认也不会去找的，<strong>所以 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>-L.</code></span>选项不能少</strong>。</p>
<p>编译器默认会找的目录可以用 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>-print-search-dirs</code></span> 选项查看：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ gcc -print-search-dirs
install: /usr/lib/gcc/i486-linux-gnu/4.3.2/
programs: =/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/:/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/:/usr/libexec/gcc/i486-linux-gnu/4.3.2/:/usr/libexec/gcc/i486-linux-gnu/:/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../i486-linux-gnu/bin/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../i486-linux-gnu/bin/
libraries: =/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../i486-linux-gnu/lib/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../i486-linux-gnu/lib/../lib/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib/:/lib/i486-linux-gnu/4.3.2/:/lib/../lib/:/usr/lib/i486-linux-gnu/4.3.2/:/usr/lib/../lib/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../i486-linux-gnu/lib/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../:/lib/:/usr/lib/
</code></span></code></pre>
<p>其中的 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>libraries</code></span> 就是库文件的搜索路径列表，各路径之间用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>:</code></span>号隔开。</p>
<p>编译器会在这些搜索路径以及 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>-L</code></span> 选项指定的路径中查找用 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>-l</code></span> 选项指定的库，比如 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>-l stack</code></span>，编译器会首<strong>先找有没有共享库 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>libstack.so</code></span>，如果有就链接它，如果没有就找有没有静态库 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>libstack.a</code></span>，如果有就链接它</strong>。</p>
<p>所以编译器是优先考虑共享库的，<strong>如果希望编译器只链接静态库，可以指定 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>-static</code></span> 选项。</strong></p>
<h5 id="_3">那么链接共享库和链接静态库有什么区别呢？</h5>
<p>在链接 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>libc</code></span> 共享库时只是指定了动态链接器和该程序所需要的库文件，并没有真的做链接，可执行文件 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span>中调用的 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>libc</code></span> 库函数仍然是未定义符号，要在 <strong>运行时做动态链接</strong>。</p>
<p>而在链接静态库时，链接器会把静态库中的目标文件取出来和可执行文件真正链接在一起。</p>
<p>我们通过反汇编看上一步生成的可执行文件<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span>：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ objdump -d main
...
08048394 &lt;main&gt;:
 8048394:       8d 4c 24 04             lea    0x4(%esp),%ecx
 8048398:       83 e4 f0                and    $0xfffffff0,%esp
 804839b:       ff 71 fc                pushl  -0x4(%ecx)
...
080483c0 &lt;push&gt;:
 80483c0:       55                      push   %ebp
 80483c1:       89 e5                   mov    %esp,%ebp
 80483c3:       83 ec 04                sub    $0x4,%esp
</code></span></code></pre>
<p>有意思的是，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main.c</code></span> 只调用了 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>push</code></span> 这一个函数，所以链接生成的可执行文件中也只有 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>push</code></span> 而没有 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>pop</code></span> 和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>is_empty</code></span>。这是使用静态库的一个好处，链接器可以从静态库中只取出需要的部分来做链接。如果是直接把那些目标文件和 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>main.c</code></span>编译链接在一起：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$gcc main.c stack.o push.o pop.o is_empty.o -Istack -o main
</code></span></code></pre>
<p>则没有用到的函数也会链接进来。当然另一个好处就是使用静态库只需写一个库文件名，而不需要写一长串目标文件名。</p>
<h3 id="53">5.3 共享库</h3>
<p>http://akaedu.github.io/book/ch20s04.html   &ldquo;共享库&rdquo;</p>
<p>组成共享库的目标文件和一般的目标文件有所不同，在编译时要加 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>-fPIC</code></span> 选项，例如：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ gcc -c -fPIC stack/stack.c stack/push.c stack/pop.c stack/is_empty.c
</code></span></code></pre>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>-f</code></span>后面跟一些编译选项，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>PIC</code></span> 是其中一种，表示生成位置无关代码（Position Independent Code）。</p>
<h5 id="-fpic">那么用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>-fPIC</code></span>生成的目标文件和一般的目标文件有什么不同呢？</h5>
<p>我们知道一般的目标文件称为Relocatable，在链接时可以把目标文件中各段的地址做重定位，重定位时需要修改指令。</p>
<p><strong>一、操作：不加 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>-fPIC</code></span> 选项编译生成目标文件：</strong></p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ gcc -c -g stack/stack.c stack/push.c stack/pop.c stack/is_empty.c
$ objdump -dS push.o # 反编译分析：objdump -dS push.o

push.o:     file format elf32-i386


Disassembly of section .text:

00000000 &lt;push&gt;:
/* push.c */
extern char stack[512];
extern int top;

void push(char c)
{
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   83 ec 04                sub    $0x4,%esp
   6:   8b 45 08                mov    0x8(%ebp),%eax
   9:   88 45 fc                mov    %al,-0x4(%ebp)
    stack[++top] = c;
# 指令中凡是用到 stack 和 top 的地址都用0x0表示，准备在重定位时修改。
   c:   a1 00 00 00 00          mov    0x0,%eax
  11:   83 c0 01                add    $0x1,%eax
  14:   a3 00 00 00 00          mov    %eax,0x0
  19:   8b 15 00 00 00 00       mov    0x0,%edx
  1f:   0f b6 45 fc             movzbl -0x4(%ebp),%eax
  23:   88 82 00 00 00 00       mov    %al,0x0(%edx)
}
  29:   c9                      leave  
  2a:   c3                      ret
</code></span></code></pre>
<p><strong>结果：</strong>指令中凡是用到<span style="overflow-x: auto; max-width:100%; display:inline;"><code>stack</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>top</code></span>的地址都用0x0表示，准备在重定位时修改。</p>
<p><strong>操作：</strong>编译链接成可执行文件</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ gcc -g main.c stack.o push.o pop.o is_empty.o -Istack -o main
$ objdump -dS main
...
080483c0 &lt;push&gt;:
/* push.c */
extern char stack[512];
extern int top;

void push(char c)
{
 80483c0:       55                      push   %ebp
 80483c1:       89 e5                   mov    %esp,%ebp
 80483c3:       83 ec 04                sub    $0x4,%esp
 80483c6:       8b 45 08                mov    0x8(%ebp),%eax
 80483c9:       88 45 fc                mov    %al,-0x4(%ebp)
        stack[++top] = c;
 80483cc:       a1 10 a0 04 08          mov    0x804a010,%eax
 80483d1:       83 c0 01                add    $0x1,%eax
 80483d4:       a3 10 a0 04 08          mov    %eax,0x804a010
 80483d9:       8b 15 10 a0 04 08       mov    0x804a010,%edx
 80483df:       0f b6 45 fc             movzbl -0x4(%ebp),%eax
 80483e3:       88 82 40 a0 04 08       mov    %al,0x804a040(%edx)
}
 80483e9:       c9                      leave  
 80483ea:       c3                      ret    
 80483eb:       90                      nop    
...
</code></span></code></pre>
<p><strong>结果：</strong>指令中的 0x0 被修改成了 0x804a010 和 0x804a040，这样做了重定位之后，各段的加载地址就定死了，因为在指令中使用了绝对地址。</p>
<p><strong>二、操作：用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>-fPIC</code></span>编译生成的目标文件：</strong></p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ gcc -c -g -fPIC stack/stack.c stack/push.c stack/pop.c stack/is_empty.c
$ objdump -dS push.o

push.o:     file format elf32-i386


Disassembly of section .text:

00000000 &lt;push&gt;:
/* push.c */
extern char stack[512];
extern int top;

void push(char c)
{
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   53                      push   %ebx
   4:   83 ec 04                sub    $0x4,%esp
   7:   e8 fc ff ff ff          call   8 &lt;push+0x8&gt;
   c:   81 c3 02 00 00 00       add    $0x2,%ebx
  12:   8b 45 08                mov    0x8(%ebp),%eax
  15:   88 45 f8                mov    %al,-0x8(%ebp)
    stack[++top] = c;
  18:   8b 83 00 00 00 00       mov    0x0(%ebx),%eax
  1e:   8b 00                   mov    (%eax),%eax
  20:   8d 50 01                lea    0x1(%eax),%edx
  23:   8b 83 00 00 00 00       mov    0x0(%ebx),%eax
  29:   89 10                   mov    %edx,(%eax)
  2b:   8b 83 00 00 00 00       mov    0x0(%ebx),%eax
  31:   8b 08                   mov    (%eax),%ecx
  33:   8b 93 00 00 00 00       mov    0x0(%ebx),%edx
  39:   0f b6 45 f8             movzbl -0x8(%ebp),%eax
  3d:   88 04 0a                mov    %al,(%edx,%ecx,1)
}
  40:   83 c4 04                add    $0x4,%esp
  43:   5b                      pop    %ebx
  44:   5d                      pop    %ebp
  45:   c3                      ret    

Disassembly of section .text.__i686.get_pc_thunk.bx:

00000000 &lt;__i686.get_pc_thunk.bx&gt;:
   0:   8b 1c 24                mov    (%esp),%ebx
   3:   c3                      ret
</code></span></code></pre>
<p><strong>结果：</strong>指令中用到的 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>stack</code></span> 和 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>top</code></span> 的地址不再以 0x0 表示，而是以 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>0x0(%ebx)</code></span> 表示，但其中还是留有 0x0 准备做进一步修改。</p>
<p><strong>操作：</strong>先编译生成共享库再做反汇编分析：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ gcc -shared -o libstack.so stack.o push.o pop.o is_empty.o
$ objdump -dS libstack.so
...
0000047c &lt;push&gt;:
/* push.c */
extern char stack[512];
extern int top;

void push(char c)
{
 47c:   55                      push   %ebp
 47d:   89 e5                   mov    %esp,%ebp
 47f:   53                      push   %ebx
 480:   83 ec 04                sub    $0x4,%esp
 483:   e8 ef ff ff ff          call   477 &lt;__i686.get_pc_thunk.bx&gt;
 488:   81 c3 6c 1b 00 00       add    $0x1b6c,%ebx
 48e:   8b 45 08                mov    0x8(%ebp),%eax
 491:   88 45 f8                mov    %al,-0x8(%ebp)
    stack[++top] = c;
# ----- 与先前指令对比 -----
# 可以发现，-0xc(%ebx) 这个地址并不是变量 top 的地址
# 这个地址的内存单元中又保存了另外一个地址，这另外一个地址才是变量 top 的地址
 494:   8b 83 f4 ff ff ff       mov    -0xc(%ebx),%eax # 是把变量 top 的地址传给 eax
 49a:   8b 00                   mov    (%eax),%eax     # 从 top 的地址中取出 top 的值传给 eax
 49c:   8d 50 01                lea    0x1(%eax),%edx
# 80483cc: a1 10 a0 04 08       mov    0x804a010,%eax
# 80483d1: 83 c0 01             add    $0x1,%eax
# ----- 与先前指令对比 -----
 49f:   8b 83 f4 ff ff ff       mov    -0xc(%ebx),%eax
 4a5:   89 10                   mov    %edx,(%eax)
 4a7:   8b 83 f4 ff ff ff       mov    -0xc(%ebx),%eax
 4ad:   8b 08                   mov    (%eax),%ecx
 4af:   8b 93 f8 ff ff ff       mov    -0x8(%ebx),%edx
 4b5:   0f b6 45 f8             movzbl -0x8(%ebp),%eax
 4b9:   88 04 0a                mov    %al,(%edx,%ecx,1)
}
 4bc:   83 c4 04                add    $0x4,%esp
 4bf:   5b                      pop    %ebx
 4c0:   5d                      pop    %ebp
 4c1:   c3                      ret    
 4c2:   90                      nop    
 4c3:   90                      nop    
...
</code></span></code></pre>
<p><strong>结果：</strong>指令中的 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>0x0(%ebx)</code></span> 被修改成 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>-0xc(%ebx)</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>-0x8(%ebx)</code></span>，而不是修改成绝对地址。所以共享库各段的加载地址并没有定死，可以加载到任意位置，因为指令中没有使用绝对地址，因此称为位置无关代码。</p>
<p><strong>图 20.3. 间接寻址</strong></p>
<p><img class="pure-img" alt="间接寻址" src="https://zromyk.gitee.io/myblog-figurebed/post/Linux C.assets/link.indirect.png" /></p>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>top</code></span> 和 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>stack</code></span> 的绝对地址保存在一个地址表中，而指令通过地址表做间接寻址，因此避免了将绝对地址写死在指令中，这也是一种避免硬编码的策略。</p>
<p><strong>把<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main.c</code></span>和共享库编译链接在一起，然后运行：</strong></p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ gcc main.c -g -L. -l stack -I stack -o main
$ ./main 
./main: error while loading shared libraries: libstack.so: cannot open shared object file: No such file or directory
</code></span></code></pre>
<p>结果出乎意料，编译的时候没问题，由于指定了<span style="overflow-x: auto; max-width:100%; display:inline;"><code>-L.</code></span>选项，编译器可以在当前目录下找到 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>libstack.so</code></span>，而运行时却说找不到 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>libstack.so</code></span>。那么 <strong>运行时在哪些路径下找共享库呢</strong>？我们先用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>ldd</code></span>命令查看可执行文件依赖于哪些共享库：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ ldd main
    # 这个共享库其实并不存在于文件系统中，它是由内核虚拟出来的共享库，所以它没有对应的路径，它负责处理系统调用。
    linux-gate.so.1 =&gt;  (0xb7f5c000) 
    # ---- 没有找到 ----
    libstack.so =&gt; not found
    # 由动态链接器 ld-linux.so.2 在做动态链接时搜索到的
    libc.so.6 =&gt; /lib/tls/i686/cmov/libc.so.6 (0xb7dcf000) 
    # 动态链接器，它的路径是在编译链接时指定的
    /lib/ld-linux.so.2 (0xb7f42000) 
</code></span></code></pre>
<p><strong>共享库的搜索路径由动态链接器决定</strong></p>
<p>从 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>ld.so(8)</code></span> 的Man Page可以查到共享库路径的搜索顺序：</p>
<ol>
<li>首先在环境变量 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>LD_LIBRARY_PATH</code></span> 所记录的路径中查找。</li>
<li>然后从缓存文件 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>/etc/ld.so.cache</code></span> 中查找。这个缓存文件由 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>ldconfig</code></span> 命令读取配置文件 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>/etc/ld.so.conf</code></span>之后生成，稍后详细解释。</li>
<li>如果上述步骤都找不到，则到默认的系统路径中查找，先是 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>/usr/lib</code></span> 然后是<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/lib</code></span>。</li>
</ol>
<p><strong>解决上述问题</strong></p>
<p>第一种方法，在运行 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span> 时通过环境变量 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>LD_LIBRARY_PATH</code></span> 把当前目录添加到共享库的搜索路径：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ LD_LIBRARY_PATH=. ./main
</code></span></code></pre>
<blockquote class="content-quote">
<p>这种方法只适合在开发中临时用一下，通常<span style="overflow-x: auto; max-width:100%; display:inline;"><code>LD_LIBRARY_PATH</code></span>是不推荐使用的，尽量不要设置这个环境变量</p>
</blockquote>
<p>第二种方法，这是最常用的方法。把 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>libstack.so</code></span> 所在目录的绝对路径（比如 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>/home/akaedu/somedir</code></span>）添加到 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>/etc/ld.so.conf</code></span>中（该文件中每个路径占一行），然后运行 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>ldconfig</code></span></p>
<p>第三种方法，把 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>libstack.so</code></span>拷到 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>/usr/lib</code></span> 或 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>/lib</code></span> 目录，这样可以确保动态链接器能找到这个共享库。</p>
<p>第四种方法，在编译可执行文件 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span> 的时候就把 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>libstack.so</code></span> 的路径写死在可执行文件中：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ gcc main.c -g -L. -l stack -I stack -o main -Wl,-rpath,/home/akaedu/somedir
# -Wl,-rpath,/home/akaedu/somedir 表示 -rpath /home/akaedu/somedir 是由 gcc 传递给链接器的选项。
</code></span></code></pre>
<h2 id="_4">六、预处理</h2>
<h3 id="61">6.1 函数式宏定义</h3>
<p>http://akaedu.github.io/book/ch21s02.html   &ldquo;宏定义&rdquo;</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#define MAX(a, b) ((a)&gt;(b)?(a):(b))
k = MAX(i&amp;0x0f, j&amp;0x0f)
</code></span></code></pre>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>gcc</code></span>的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>-E</code></span>选项或<span style="overflow-x: auto; max-width:100%; display:inline;"><code>cpp</code></span>命令进行预处理：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ cpp main.c
# 1 &quot;main.c&quot;
# 1 &quot;&lt;built-in&gt;&quot;
# 1 &quot;&lt;command-line&gt;&quot;
# 1 &quot;main.c&quot;

k = ((i&amp;0x0f)&gt;(j&amp;0x0f)?(i&amp;0x0f):(j&amp;0x0f))
</code></span></code></pre>
<p>函数式宏定义经常写成这样的形式（取自内核代码<span style="overflow-x: auto; max-width:100%; display:inline;"><code>include/linux/pm.h</code></span>）：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#define device_init_wakeup(dev,val) \
        do { \
                device_can_wakeup(dev) = !!(val); \
                device_set_wakeup_enable(dev,val); \
        } while(0)
</code></span></code></pre>
<details>
    <summary>为什么要用 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>do { ... } while(0)</code></span> 括起来呢？不括起来会有什么问题呢？</summary>
    <pre><code class="pre-wrap">
#define device_init_wakeup(dev,val) \
            device_can_wakeup(dev) = !!(val); \
            device_set_wakeup_enable(dev,val);
if (n > 0)
    device_init_wakeup(d, v);</code></pre>
    <p>这样宏展开之后，函数体的第二条语句不在if条件中。</p>
</details>

<details>
    <summary>那么简单地用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>{ ... }</code></span>括起来组成一个语句块不行吗？ </summary>
    <pre><code class="pre-wrap">
#define device_init_wakeup(dev,val) \
                { device_can_wakeup(dev) = !!(val); \
                device_set_wakeup_enable(dev,val); }
if (n > 0)
    device_init_wakeup(d, v);
else
    continue;</code></pre>
    <p>问题出在<span style="overflow-x: auto; max-width:100%; display:inline;"><code>device_init_wakeup(d, v);</code></span>末尾的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>;</code></span>号，如果不允许写这个<span style="overflow-x: auto; max-width:100%; display:inline;"><code>;</code></span>号，看起来不像个函数调用，可如果写了这个<span style="overflow-x: auto; max-width:100%; display:inline;"><code>;</code></span>号，宏展开之后就有语法错误，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>if</code></span>语句被这个<span style="overflow-x: auto; max-width:100%; display:inline;"><code>;</code></span>号结束掉了，没法跟<span style="overflow-x: auto; max-width:100%; display:inline;"><code>else</code></span>配对。因此，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>do { ... } while(0)</code></span>是一种比较好的解决办法。</p>
</details>

<p>如果在一个程序文件中重复定义一个宏，C语言规定这些重复的宏定义必须一模一样。例如这样的重复定义是允许的：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#define OBJ_LIKE (1 - 1)
#define OBJ_LIKE /* comment */ (1/* comment */-/* comment */  1)/* comment */
/* 上面两个宏定义事一模一样 */
</code></span></code></pre>
<p>在定义的前后多些空白（空格、Tab、注释）没有关系，在定义之中多些空白或少些空白也没有关系，但在定义之中有空白和没有空白被认为是不同的，所以这样的重复定义是不允许的：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#define OBJ_LIKE (1 - 1)
#define OBJ_LIKE (1-1) /* 不允许 */
</code></span></code></pre>
<p>如果需要重新定义一个宏，和原来的定义不同，可以先用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>#undef</code></span>取消原来的定义，再重新定义，例如：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#define X 3
... /* X is 3 */
#undef X
... /* X has no definition */
#define X 2
... /* X is 2 */
</code></span></code></pre>
<h3 id="62-c99">6.2 内联函数（C99）</h3>
<p>C99引入一个新关键字<span style="overflow-x: auto; max-width:100%; display:inline;"><code>inline</code></span>，用于定义内联函数（inline function）。这种用法在内核代码中很常见，例如<span style="overflow-x: auto; max-width:100%; display:inline;"><code>include/linux/rwsem.h</code></span>中：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">static inline void down_read(struct rw_semaphore *sem)
{
        might_sleep();
        rwsemtrace(sem,&quot;Entering down_read&quot;);
        __down_read(sem);
        rwsemtrace(sem,&quot;Leaving down_read&quot;);
}
</code></span></code></pre>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>inline</code></span>关键字告诉编译器，这个函数的调用要尽可能快，可以当普通的函数调用实现，也可以用宏展开的办法实现。</p>
<h3 id="63">6.3 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>#</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>##</code></span>运算符和可变参数</h3>
<p>在函数式宏定义中，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>#</code></span>运算符用于创建字符串，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>#</code></span>运算符后面应该跟一个形参（中间可以有空格或Tab），例如：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#define STR(s) # s
STR(hello   world)
</code></span></code></pre>
<p>用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>cpp</code></span>命令预处理之后是<span style="overflow-x: auto; max-width:100%; display:inline;"><code>"hello␣world"</code></span>，自动用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>"</code></span>号把实参括起来成为一个字符串，并且实参中的连续多个空白字符被替换成一个空格。</p>
<p>再比如：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#define STR(s) #s
fputs(STR(strncmp(&quot;ab\&quot;c\0d&quot;, &quot;abc&quot;, '\4&quot;') == 0) STR(: @\n), s);
</code></span></code></pre>
<h2 id="_5">算法</h2>
<p>http://akaedu.github.io/book/ch11s02.html   &ldquo;例 11.1. 插入排序&rdquo;<br />
http://akaedu.github.io/book/ch11s04.html   &ldquo;例 11.2. 归并排序&rdquo;<br />
http://akaedu.github.io/book/ch11s05.html   &ldquo;例 11.3. 线性查找&rdquo;<br />
http://akaedu.github.io/book/ch11s06.html   &ldquo;例 11.4. 折半查找&rdquo;</p>
<h2 id="c_1">C语言关键字</h2>
<h3 id="volatile"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>volatile</code></span> 限定符</h3>
<p><strong>例 19.7. <span style="overflow-x: auto; max-width:100%; display:inline;"><code>volatile</code></span>限定符</strong></p>
<p>http://akaedu.github.io/book/ch19s06.html   &ldquo;例 19.7. volatile限定符&rdquo;</p>
<p>设备寄存器往往具有以下特性：</p>
<ul>
<li>设备寄存器中的数据不需要改写就可以自己发生变化，每次读上来的值都可能不一样。</li>
<li>连续多次向设备寄存器中写数据并不是在做无用功，而是有特殊意义的。</li>
</ul>
<p>用优化选项编译生成的指令明显效率更高，但使用不当会出错，为了避免编译器自作聪明，把不该优化的也优化了，程序员应该明确告诉编译器哪些内存单元的访问是不能优化的，<strong>在 C 语言中可以用 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>volatile</code></span> 限定符修饰变量，就是告诉编译器，即使在编译时指定了优化选项，每次读这个变量仍然要老老实实从内存读取，每次写这个变量也仍然要老老实实写回内存</strong>，不能省略任何步骤。</p>
<h5 id="gcc"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>gcc</code></span> 的编译优化选项</h5>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>gcc</code></span> 的编译优化选项有<span style="overflow-x: auto; max-width:100%; display:inline;"><code>-O0</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>-O</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>-O1</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>-O2</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>-O3</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>-Os</code></span>几种。<span style="overflow-x: auto; max-width:100%; display:inline;"><code>-O0</code></span>表示不优化，这是缺省的选项。<span style="overflow-x: auto; max-width:100%; display:inline;"><code>-O1</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>-O2</code></span> 和 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>-O3</code></span> 这几个选项一个比一个优化得更多，编译时间也更长。<span style="overflow-x: auto; max-width:100%; display:inline;"><code>-O</code></span> 和 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>-O1</code></span> 相同。<span style="overflow-x: auto; max-width:100%; display:inline;"><code>-Os</code></span> 表示为缩小目标文件的尺寸而优化。具体每种选项做了哪些优化请参考 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>gcc(1)</code></span> 的Man Page。</p>
<p>从上面的例子还可以看到，如果在编译时指定了优化选项，源代码和生成指令的次序可能无法对应，甚至有些源代码可能不对应任何指令，被彻底优化掉了。这一点在用 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>gdb</code></span> 做源码级调试时尤其需要注意（做指令级调试没关系），在为调试而编译时不要指定优化选项，否则可能无法一步步跟踪源代码的执行过程。</p>
<p><strong>有了 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>volatile</code></span> 限定符，是可以防止编译器优化对设备寄存器的访问，但是对于有 Cache 的平台，仅仅这样还不够，还是无法防止 Cache 优化对设备寄存器的访问。</strong></p>
<p>在访问普通的内存单元时，Cache 对程序员是透明的，在读写设备寄存器时Cache的存在就不容忽视了，如果串口发送寄存器的地址被 Cahce 缓存，CPU 执行单元对串口发送寄存器做写操作都写到 Cache 中去了，串口发送寄存器并没有及时得到数据，也就不能及时发送，与此类似，如果串口接收寄存器的地址被 Cache 缓存，串口接收寄存器当作普通内存单元，并且相信内存单元中的数据是不会自己变的，以后每次读串口接收寄存器时，Cache都会把缓存的第 1 个字节提供给 CPU 执行单元。</p>
<p>通常，有 <strong>Cache 的平台都有办法对某一段地址范围禁用 Cache</strong>，一般是在页表中设置的，可以设定哪些页面允许Cache 缓存，哪些页面不允许 Cache 缓存，MMU 不仅要做地址转换和访问权限检查，也要和 Cache 协同工作。</p>
<p><strong>除了设备寄存器需要用 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>volatile</code></span> 限定之外，当一个全局变量被同一进程中的多个控制流程访问时也要用 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>volatile</code></span> 限定，比如信号处理函数和多线程。</strong></p>
<h3 id="restrict"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>restrict</code></span></h3>
<p>由 <strong>程序员保证</strong> 使用关键字 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>restrict</code></span> 的两个指针不会改变同一地址的内存。</p>
<p>http://en.cppreference.com/w/c/language/restrict    &ldquo;restrict type qualifier&rdquo;<br />
https://www.zhihu.com/question/41653775 &ldquo;如何理解C语言关键字restrict？&rdquo;</p>
<h2 id="28-io">第 28 章 文件与I/O</h2>
<h3 id="1-hello-world">1. 汇编程序的Hello world</h3>
<p>例 28.1. 汇编程序的Hello world</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">.data             # section declaration

# 标号msg，代表字符串&quot;Hello, world!\n&quot;的首地址，相当于C程序的一个全局变量
# 注意在C语言中字符串的末尾隐含有一个'\0'，而汇编指示.ascii定义的字符串末尾没有隐含的'\0'
msg: 
  .ascii  &quot;Hello, world!\n&quot; # our dear string
  len = . - msg         # length of our dear string

.text             # section declaration

                # we must export the entry point to the ELF linker or
    .global _start        # loader. They conventionally recognize _start as their
                # entry point. Use ld -e foo to override the default.

# 在_start中调了两个系统调用
_start:

# write our string to stdout

  movl  $len,%edx # third argument: message length
  movl  $msg,%ecx # second argument: pointer to message to write
  movl  $1,%ebx   # first argument: file handle (stdout)
  movl  $4,%eax   # system call number (sys_write) 
  int   $0x80   # call kernel

  # 在调write系统调用时，eax寄存器保存着write的系统调用号4
  # ebx、ecx、edx寄存器分别保存着write系统调用需要的三个参数
  # ebx保存着文件描述符，文件描述符1表示标准输出，对应于C标准I/O库的stdout
  # ecx保存着输出缓冲区的首地址
  # edx保存着输出的字节数

# and exit

  movl  $0,%ebx   # first argument: exit code
  movl  $1,%eax   # system call number (sys_exit)
  int   $0x80   # call kernel
</code></span></code></pre>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>$ as -o hello.o hello.s
$ ld -o hello hello.o
$ ./hello
Hello, world!
</code></span></code></pre>
<p>这段汇编相当于以下C代码：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;unistd.h&gt;

char msg[14] = &quot;Hello, world!\n&quot;;
#define len 14

int main(void)
{
    /*  C代码中的write函数是系统调用的包装函数
      其内部实现就是把传进来的三个参数分别赋给ebx、ecx、edx寄存器，
      然后执行movl $4,%eax和int $0x80两条指令。

      这个函数不可能完全用C代码来写，因为任何C代码都不会编译生成int指令，
      所以这个函数有可能是完全用汇编写的，也可能是用C内联汇编写的，
      甚至可能是一个宏定义（省了参数入栈出栈的步骤）。
    */
  write(1, msg, len);
  _exit(0); // _exit函数也是如此，我们讲过这些系统调用的包装函数位于Man Page的第2个Section。
}
</code></span></code></pre>
<h3 id="2-ciounbuffered-io">2. C标准I/O库函数与Unbuffered I/O函数</h3>
<p>现在看看C标准I/O库函数是如何用系统调用实现的。</p>
<p>-<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fopen(3)</code></span></p>
<p>调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>open(2)</code></span>打开指定的文件，返回一个文件描述符（就是一个<span style="overflow-x: auto; max-width:100%; display:inline;"><code>int</code></span>类型的编号），分配一个<span style="overflow-x: auto; max-width:100%; display:inline;"><code>FILE</code></span>结构体，其中包含该文件的描述符、I/O 缓冲区和当前读写位置等信息，返回这个<span style="overflow-x: auto; max-width:100%; display:inline;"><code>FILE</code></span>结构体的地址。</p>
<p>-<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fgetc(3)</code></span></p>
<p>通过传入的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>FILE *</code></span>参数找到该文件的描述符、I/O缓冲区和当前读写位置，判断能否从I/O缓冲区中读到下一个字符，如果能读到就直接返回该字符，否则调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>read(2)</code></span>，把文件描述符传进去，让内核读取该文件的数据到I/O缓冲区，然后返回下一个字符。注意，对于C标准I/O库来说，打开的文件由<span style="overflow-x: auto; max-width:100%; display:inline;"><code>FILE *</code></span>指针标识，而对于内核来说，打开的文件由文件描述符标识，文件描述符从<span style="overflow-x: auto; max-width:100%; display:inline;"><code>open</code></span>系统调用获得，在使用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>read</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>write</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>close</code></span>系统调用时都需要传文件描述符。</p>
<p>-<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fputc(3)</code></span></p>
<p>判断该文件的 I/O 缓冲区是否有空间再存放一个字符，如果有空间则直接保存在 I/O 缓冲区中并返回，如果I/O 缓冲区已满就调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>write(2)</code></span>，让内核把 I/O 缓冲区的内容写回文件。</p>
<p>-<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fclose(3)</code></span></p>
<p>如果 I/O 缓冲区中还有数据没写回文件，就调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>write(2)</code></span>写回文件，然后调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>close(2)</code></span>关闭文件，释放<span style="overflow-x: auto; max-width:100%; display:inline;"><code>FILE</code></span>结构体和 I/O 缓冲区。</p>
<p>程序启动时会自动打开三个文件：标准输入、标准输出和标准错误输出。在C标准库中分别用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>FILE *</code></span>指针<span style="overflow-x: auto; max-width:100%; display:inline;"><code>stdin</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>stdout</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>stderr</code></span>表示。这三个文件的描述符分别是0、1、2，保存在相应的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>FILE</code></span>结构体中。头文件<span style="overflow-x: auto; max-width:100%; display:inline;"><code>unistd.h</code></span>中有如下的宏定义来表示这三个文件描述符：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
</code></span></code></pre>
<h3 id="3-openclose">3. open/close</h3>
<p>在C代码中<span style="overflow-x: auto; max-width:100%; display:inline;"><code>open</code></span>函数是这样声明的：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">int open(const char *pathname, int flags, ...);
</code></span></code></pre>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>close</code></span>函数关闭一个已打开的文件：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;unistd.h&gt;

int close(int fd);
// 返回值：成功返回0，出错返回-1并设置errno
</code></span></code></pre>
<p>需要说明的是，<strong>当一个进程终止时，内核对该进程所有尚未关闭的文件描述符调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>close</code></span>关闭</strong>，所以即使用户程序不调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>close</code></span>，在终止时内核也会自动关闭它打开的所有文件。但是对于一个长年累月运行的程序（比如网络服务器），打开的文件描述符一定要记得关闭，否则随着打开的文件越来越多，会占用大量文件描述符和系统资源。</p>
<h5 id="printf">printf 重定向</h5>
<p><strong>由<span style="overflow-x: auto; max-width:100%; display:inline;"><code>open</code></span>返回的文件描述符一定是该进程尚未使用的最小描述符。</strong>由于程序启动时自动打开文件描述符0、1、2，因此第一次调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>open</code></span>打开文件通常会返回描述符3，再调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>open</code></span>就会返回4。可以利用这一点在标准输入、标准输出或标准错误输出上打开一个新文件，实现重定向的功能。例如，首先调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>close</code></span>关闭文件描述符1，然后调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>open</code></span>打开一个常规文件，则一定会返回文件描述符1，这时候标准输出就不再是终端，而是一个常规文件了，再调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>printf</code></span>就不会打印到屏幕上，而是写到这个文件中了。</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">close(stdin);
open(const char *pathname, int flags, ...); // 返回文件描述符 1
print(...);
</code></span></code></pre>
<h3 id="4-readwrite">4. read/write</h3>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>read</code></span>函数从打开的设备或文件中读取数据。</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;unistd.h&gt;

ssize_t read(int fd, void *buf, size_t count);
// 返回值：成功返回读取的字节数
// 出错返回-1并设置errno
// 如果在调read之前已到达文件末尾，则这次read返回0
</code></span></code></pre>
<ul>
<li>读常规文件时，在读到<span style="overflow-x: auto; max-width:100%; display:inline;"><code>count</code></span>个字节之前已到达文件末尾。例如，距文件末尾还有30个字节而请求读100个字节，则<span style="overflow-x: auto; max-width:100%; display:inline;"><code>read</code></span>返回30，下次<span style="overflow-x: auto; max-width:100%; display:inline;"><code>read</code></span>将返回0。</li>
<li>从终端设备读，通常以行为单位，读到换行符就返回了。</li>
<li>从网络读，根据不同的传输层协议和内核缓存机制，返回值可能小于请求的字节数，后面socket编程部分会详细讲解。</li>
</ul>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>write</code></span>函数向打开的设备或文件中写数据。</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;unistd.h&gt;

ssize_t write(int fd, const void *buf, size_t count);
// 返回值：成功返回写入的字节数
// 出错返回-1并设置errno
</code></span></code></pre>
<p>写常规文件时，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>write</code></span>的返回值通常等于请求写的字节数<span style="overflow-x: auto; max-width:100%; display:inline;"><code>count</code></span>，而向终端设备或网络写则不一定。</p>
<p>读常规文件是不会阻塞的，不管读多少字节，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>read</code></span>一定会在有限的时间内返回。从终端设备或网络读则不一定，<strong>如果从终端输入的数据没有换行符，调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>read</code></span>读终端设备就会阻塞，如果网络上没有接收到数据包，调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>read</code></span>从网络读就会阻塞</strong>，至于会阻塞多长时间也是不确定的，如果一直没有数据到达就一直阻塞在那里。同样，写常规文件是不会阻塞的，而向终端设备或网络写则不一定。</p>
<p><strong>例 28.2. 阻塞读终端</strong></p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;unistd.h&gt;
#include &lt;stdlib.h&gt;

int main(void)
{
  char buf[10];
  int n;
  n = read(STDIN_FILENO, buf, 10);
  if (n &lt; 0) {
    perror(&quot;read STDIN_FILENO&quot;);
    exit(1);
  }
  write(STDOUT_FILENO, buf, n);
  return 0;
}
</code></span></code></pre>
<p>执行结果如下：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>$ ./a.out 
hello（回车）
hello
$ ./a.out 
hello world（回车）
hello worl$ d
bash: d: command not found
</code></span></code></pre>
<p>第一次执行<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a.out</code></span>的结果很正常，而第二次执行的过程有点特殊，现在分析一下：</p>
<ol>
<li>Shell 进程创建<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a.out</code></span>进程，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a.out</code></span>进程开始执行，而Shell进程睡眠等待<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a.out</code></span>进程退出。<br />
    2.<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a.out</code></span>调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>read</code></span>时睡眠等待，直到终端设备输入了换行符才从<span style="overflow-x: auto; max-width:100%; display:inline;"><code>read</code></span>返回，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>read</code></span>只读走10个字符，剩下的字符仍然保存在内核的终端设备输入缓冲区中。<br />
    3.<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a.out</code></span>进程打印并退出，这时 Shell 进程恢复运行，Shell 继续从终端读取用户输入的命令，于是读走了终端设备输入缓冲区中剩下的字符 d 和换行符，把它当成一条命令解释执行，结果发现执行不了，没有 d 这个命令。</li>
</ol>
<h3 id="5-lseek">5. lseek</h3>
<p>每个打开的文件都记录着当前读写位置，打开文件时读写位置是0，表示文件开头，通常读写多少个字节就会将读写位置往后移多少个字节。但是有一个例外，如果以<span style="overflow-x: auto; max-width:100%; display:inline;"><code>O_APPEND</code></span>方式打开，每次写操作都会在文件末尾追加数据，然后将读写位置移到新的文件末尾。<span style="overflow-x: auto; max-width:100%; display:inline;"><code>lseek</code></span>和标准I/O库的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fseek</code></span>函数类似，可以移动当前读写位置（或者叫偏移量）。</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>#include &lt;sys/types.h&gt;
#include &lt;unistd.h&gt;

off_t lseek(int fd, off_t offset, int whence);
</code></span></code></pre>
<p>参数<span style="overflow-x: auto; max-width:100%; display:inline;"><code>offset</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>whence</code></span>的含义和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fseek</code></span>函数完全相同。只不过第一个参数换成了文件描述符。和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fseek</code></span>一样，偏移量允许超过文件末尾，这种情况下对该文件的下一次写操作将延长文件，中间空洞的部分读出来都是0。</p>
<h3 id="6-fcntl">6. fcntl</h3>
<h3 id="7-ioctl">7. ioctl</h3>
<h3 id="8-mmap">8. mmap</h3>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>mmap</code></span>可以 <strong>把磁盘文件的一部分直接映射到内存</strong>，这样文件中的位置直接就有对应的内存地址，对文件的读写可以直接用指针来做而不需要<span style="overflow-x: auto; max-width:100%; display:inline;"><code>read</code></span>/<span style="overflow-x: auto; max-width:100%; display:inline;"><code>write</code></span>函数。</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;sys/mman.h&gt;

void *mmap(void *addr, size_t len, int prot, int flag, int filedes, off_t off);
int munmap(void *addr, size_t len);
</code></span></code></pre>
<p>-<span style="overflow-x: auto; max-width:100%; display:inline;"><code>addr</code></span>参数</p>
<ul>
<li>如果<span style="overflow-x: auto; max-width:100%; display:inline;"><code>addr</code></span>参数为<span style="overflow-x: auto; max-width:100%; display:inline;"><code>NULL</code></span>，内核会自己在进程地址空间中选择合适的地址建立映射。</li>
<li>如果<span style="overflow-x: auto; max-width:100%; display:inline;"><code>addr</code></span>不是<span style="overflow-x: auto; max-width:100%; display:inline;"><code>NULL</code></span>，则给内核一个提示，应该从什么地址开始映射，内核会选择<span style="overflow-x: auto; max-width:100%; display:inline;"><code>addr</code></span>之上的某个合适的地址开始映射。</li>
<li>建立映射后，真正的映射首地址通过返回值可以得到。<br />
    -<span style="overflow-x: auto; max-width:100%; display:inline;"><code>len</code></span>参数是需要映射的那一部分文件的长度。<br />
    -<span style="overflow-x: auto; max-width:100%; display:inline;"><code>prot</code></span>参数有四种取值：</li>
<li>PROT_EXEC 表示映射的这一段可执行，例如映射共享库</li>
<li>PROT_READ 表示映射的这一段可读</li>
<li>PROT_WRITE 表示映射的这一段可写</li>
<li>PROT_NONE 表示映射的这一段不可访问<br />
    -<span style="overflow-x: auto; max-width:100%; display:inline;"><code>flag</code></span>参数有很多种取值，这里只讲两种，其它取值可查看 mmap(2)</li>
<li>MAP_SHARED 多个进程对同一个文件的映射是共享的，一个进程对映射的内存做了修改，另一个进程也会看到这种变化。</li>
<li>MAP_PRIVATE 多个进程对同一个文件的映射不是共享的，一个进程对映射的内存做了修改，另一个进程并不会看到这种变化，也不会真的写到文件中去。<br />
    -<span style="overflow-x: auto; max-width:100%; display:inline;"><code>filedes</code></span>是代表该文件的描述符。<br />
    -<span style="overflow-x: auto; max-width:100%; display:inline;"><code>off</code></span>参数是从文件的什么位置开始映射，必须是页大小的整数倍（在32位体系统结构上通常是4K）。</li>
</ul>
<p>如果<span style="overflow-x: auto; max-width:100%; display:inline;"><code>mmap</code></span>成功则返回映射首地址，如果出错则返回常数<span style="overflow-x: auto; max-width:100%; display:inline;"><code>MAP_FAILED</code></span>。</p>
<p>当进程终止时，该进程的映射内存会自动解除，也可以调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>munmap</code></span>解除映射。<span style="overflow-x: auto; max-width:100%; display:inline;"><code>munmap</code></span>成功返回0，出错返回-1。</p>
<h2 id="29">第 29 章 文件系统</h2>
<h3 id="2-ext2">2. ext2文件系统</h3>
<h5 id="21">2.1. 总体存储布局</h5>
<h5 id="22">2.2. 实例剖析</h5>
<h5 id="23">2.3. 数据块寻址</h5>
<h5 id="24">2.4. 文件和目录操作的系统函数</h5>
<h3 id="3-vfs">3. VFS</h3>
<h5 id="31">3.1. 内核数据结构</h5>
<h5 id="32-dupdup2">3.2. dup和dup2函数</h5>
<h2 id="30">30. 进程</h2>
<h3 id="1">1. 引言</h3>
<p>我们知道，每个进程在内核中都有一个进程控制块（PCB）来维护进程相关的信息。</p>
<p>Linux内核的进程控制块是<span style="overflow-x: auto; max-width:100%; display:inline;"><code>task_struct</code></span>结构体。</p>
<ul>
<li>进程 id。系统中每个进程有唯一的id，在C语言中用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>pid_t</code></span>类型表示，其实就是一个非负整数。</li>
<li>进程的状态，有运行、挂起、停止、僵尸等状态。</li>
<li>进程切换时需要保存和恢复的一些CPU寄存器。</li>
<li>描述虚拟地址空间的信息。</li>
<li>描述控制终端的信息。</li>
<li>当前工作目录（Current Working Directory）。<br />
  -<span style="overflow-x: auto; max-width:100%; display:inline;"><code>umask</code></span>掩码。</li>
<li>文件描述符表，包含很多指向<span style="overflow-x: auto; max-width:100%; display:inline;"><code>file</code></span>结构体的指针。</li>
<li>和信号相关的信息。</li>
<li>用户 id 和组 id。</li>
<li>控制终端、Session 和进程组。</li>
<li>进程可以使用的资源上限（Resource Limit）。</li>
</ul>
<p><strong><span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exec</code></span>是本章要介绍的两个重要的系统调用。</strong></p>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>的作用是根据一个现有的进程复制出一个新进程，原来的进程称为父进程（Parent Process），新进程称为子进程（Child Process）。系统中同时运行着很多进程，这些进程都是从最初只有一个进程开始一个一个复制出来的。</p>
<p><strong>在 Shell 下输入命令可以运行一个程序，是因为 Shell 进程在读取用户输入的命令之后会调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>复制出一个新的 Shell 进程，然后新的 Shell 进程调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exec</code></span>执行新的程序。</strong></p>
<p>我们知道一个程序可以多次加载到内存，成为同时运行的多个进程，例如可以同时开多个终端窗口运行<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/bin/bash</code></span>，另一方面，一个进程在调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exec</code></span>前后也可以分别执行两个不同的程序，例如在 Shell 提示符下输入命令<span style="overflow-x: auto; max-width:100%; display:inline;"><code>ls</code></span>，首先<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>创建子进程，这时子进程仍在执行<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/bin/bash</code></span>程序，然后子进程调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exec</code></span>执行新的程序<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/bin/ls</code></span>，如下图所示。</p>
<p><img class="pure-img" alt="fork/exec" src="https://zromyk.gitee.io/myblog-figurebed/post/Linux C.assets/process.forkexec.png" /></p>
<h3 id="2">2. 环境变量</h3>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>exec</code></span>系统调用执行新程序时会把命令行参数和环境变量表传递给<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span>函数。</p>
<p><strong>图 30.2. 进程地址空间</strong></p>
<p><img class="pure-img" alt="进程地址空间" src="https://zromyk.gitee.io/myblog-figurebed/post/Linux C.assets/process.addressspace.png" /></p>
<p>和命令行参数<span style="overflow-x: auto; max-width:100%; display:inline;"><code>argv</code></span>类似，环境变量表也是一组字符串，如下图所示。</p>
<p><strong>图 30.3. 环境变量</strong></p>
<p><img class="pure-img" alt="环境变量" src="https://zromyk.gitee.io/myblog-figurebed/post/Linux C.assets/process.environ.png" /></p>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>libc</code></span>中定义的全局变量<span style="overflow-x: auto; max-width:100%; display:inline;"><code>environ</code></span>指向环境变量表，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>environ</code></span>没有包含在任何头文件中，所以在使用时要用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>extern</code></span>声明。例如：</p>
<p><strong>例 30.1. 打印环境变量</strong></p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;stdio.h&gt;

int main(void)
{
  extern char **environ;
  int i;
  for(i=0; environ[i]!=NULL; i++)
    printf(&quot;%s\n&quot;, environ[i]);
  return 0;
}
</code></span></code></pre>
<p>执行结果为</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>$ ./a.out 
SSH_AGENT_PID=5717
SHELL=/bin/bash
DESKTOP_STARTUP_ID=
TERM=xterm
...
</code></span></code></pre>
<p>由于父进程在调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>创建子进程时会把自己的环境变量表也复制给子进程，所以<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a.out</code></span>打印的环境变量和 Shell 进程的环境变量是相同的。</p>
<h5 id="21_1">2.1 一些比较重要的环境变量</h5>
<p>按照惯例，环境变量字符串都是<span style="overflow-x: auto; max-width:100%; display:inline;"><code>name=value</code></span>这样的形式，大多数<span style="overflow-x: auto; max-width:100%; display:inline;"><code>name</code></span>由大写字母加下划线组成，一般把<span style="overflow-x: auto; max-width:100%; display:inline;"><code>name</code></span>的部分叫做环境变量，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>value</code></span>的部分则是环境变量的值。环境变量定义了进程的运行环境，一些比较重要的环境变量的含义如下：</p>
<div class="pure-table-scrollable"><table class="pure-table pure-table-horizontal">
<thead>
<tr>
<th>环境变量</th>
<th>含义</th>
</tr>
</thead>
<tbody>
<tr>
<td>PATH</td>
<td>可执行文件的搜索路径。<br /><span style="overflow-x: auto; max-width:100%; display:inline;"><code>ls</code></span>命令也是一个程序，执行它不需要提供完整的路径名<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/bin/ls</code></span>，然而通常我们执行当前目录下的程序<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a.out</code></span>却需要提供完整的路径名<span style="overflow-x: auto; max-width:100%; display:inline;"><code>./a.out</code></span>，这是因为<span style="overflow-x: auto; max-width:100%; display:inline;"><code>PATH</code></span>环境变量的值里面包含了<span style="overflow-x: auto; max-width:100%; display:inline;"><code>ls</code></span>命令所在的目录<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/bin</code></span>，却不包含<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a.out</code></span>所在的目录。<br /><span style="overflow-x: auto; max-width:100%; display:inline;"><code>PATH</code></span>环境变量的值可以包含多个目录，用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>:</code></span>号隔开。在 Shell 中用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>echo</code></span>命令可以查看这个环境变量的值：<span style="overflow-x: auto; max-width:100%; display:inline;"><code>$ echo $PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games</code></span></td>
</tr>
<tr>
<td>SHELL</td>
<td>当前 Shell，它的值通常是<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/bin/bash</code></span>。</td>
</tr>
<tr>
<td>TERM</td>
<td>当前终端类型，在图形界面终端下它的值通常是<span style="overflow-x: auto; max-width:100%; display:inline;"><code>xterm</code></span>，终端类型决定了一些程序的输出显示方式，比如图形界面终端可以显示汉字，而字符终端一般不行。</td>
</tr>
<tr>
<td>LANG</td>
<td>语言和 locale，决定了字符编码以及时间、货币等信息的显示格式。</td>
</tr>
<tr>
<td>HOME</td>
<td>当前用户主目录的路径，很多程序需要在主目录下保存配置文件，使得每个用户在运行该程序时都有自己的一套配置。</td>
</tr>
</tbody>
</table></div>
<h5 id="22-getenv">2.2 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>getenv</code></span>函数</h5>
<p>用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>environ</code></span>指针可以查看所有环境变量字符串，但是不够方便，如果给出<span style="overflow-x: auto; max-width:100%; display:inline;"><code>name</code></span>要在环境变量表中查找它对应的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>value</code></span>，可以用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>getenv</code></span>函数。</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;stdlib.h&gt;
char *getenv(const char *name);
</code></span></code></pre>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>getenv</code></span>的返回值是指向<span style="overflow-x: auto; max-width:100%; display:inline;"><code>value</code></span>的指针，若未找到则为<span style="overflow-x: auto; max-width:100%; display:inline;"><code>NULL</code></span>。</p>
<h5 id="23-setenv">2.3 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>setenv</code></span>函数</h5>
<p>修改环境变量可以用以下函数：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;stdlib.h&gt;

int setenv(const char *name, const char *value, int rewrite);
void unsetenv(const char *name);
</code></span></code></pre>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>putenv</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>setenv</code></span>函数若成功则返回为0，若出错则返回非0。</p>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>setenv</code></span>将环境变量<span style="overflow-x: auto; max-width:100%; display:inline;"><code>name</code></span>的值设置为<span style="overflow-x: auto; max-width:100%; display:inline;"><code>value</code></span>。如果已存在环境变量<span style="overflow-x: auto; max-width:100%; display:inline;"><code>name</code></span>，那么</p>
<ul>
<li>若 rewrite 非 0，则覆盖原来的定义；</li>
<li>若 rewrite 为 0，则不覆盖原来的定义，也不返回错误。</li>
</ul>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>unsetenv</code></span>删除<span style="overflow-x: auto; max-width:100%; display:inline;"><code>name</code></span>的定义。即使<span style="overflow-x: auto; max-width:100%; display:inline;"><code>name</code></span>没有定义也不返回错误。</p>
<p><strong>例 30.2. 修改环境变量</strong></p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;stdlib.h&gt;
#include &lt;stdio.h&gt;

int main(void)
{
  printf(&quot;PATH=%s\n&quot;, getenv(&quot;PATH&quot;));
  setenv(&quot;PATH&quot;, &quot;hello&quot;, 1);
  printf(&quot;PATH=%s\n&quot;, getenv(&quot;PATH&quot;));
  return 0;
}
</code></span></code></pre>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>$ ./a.out 
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
PATH=hello
$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
</code></span></code></pre>
<p>可以看出，Shell 进程的环境变量<span style="overflow-x: auto; max-width:100%; display:inline;"><code>PATH</code></span>传给了<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a.out</code></span>，然后<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a.out</code></span>修改了<span style="overflow-x: auto; max-width:100%; display:inline;"><code>PATH</code></span>的值，在<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a.out</code></span>中能打印出修改后的值，但在 Shell 进程中<span style="overflow-x: auto; max-width:100%; display:inline;"><code>PATH</code></span>的值没变。<strong>父进程在创建子进程时会复制一份环境变量给子进程，但此后二者的环境变量互不影响。</strong></p>
<h3 id="3">3. 进程控制</h3>
<h5 id="31-fork">3.1. fork函数</h5>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;sys/types.h&gt;
#include &lt;unistd.h&gt;

pid_t fork(void);
</code></span></code></pre>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>调用失败则返回-1，调用成功的返回值见下面的解释。我们通过一个例子来理解<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>是怎样创建新进程的。</p>
<p><strong>例 30.3. fork</strong></p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;sys/types.h&gt;
#include &lt;unistd.h&gt;
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;

int main(void)
{
  pid_t pid;
  char *message;
  int n;
  pid = fork();
  if (pid &lt; 0) {
    perror(&quot;fork failed&quot;);
    exit(1);
  }
  if (pid == 0) {
    message = &quot;This is the child\n&quot;;
    n = 6;
  } else {
    message = &quot;This is the parent\n&quot;;
    n = 3;
  }
  for(; n &gt; 0; n--) {
    printf(message);
    sleep(1);
  }
  return 0;
}
</code></span></code></pre>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>$ ./a.out 
This is the child
This is the parent
This is the child
This is the parent
This is the child
This is the parent
This is the child
$ This is the child
This is the child
</code></span></code></pre>
<p>这个程序的运行过程如下图所示。</p>
<p><img class="pure-img" alt="fork" src="https://zromyk.gitee.io/myblog-figurebed/post/Linux C.assets/process.fork.png" /></p>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>函数的特点概括起来就是“调用一次，返回两次”，在父进程中调用一次，在父进程和子进程中各返回一次。</p>
<p><strong>子进程中<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>的返回值是0，而父进程中<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>的返回值则是子进程的 id</strong>（从根本上说<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>是从内核返回的，内核自有办法让父进程和子进程返回不同的值），这样当<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>函数返回后，程序员可以根据返回值的不同让父进程和子进程执行不同的代码。</p>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>的返回值这样规定是有道理的。</p>
<ul>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>在子进程中返回0，子进程仍可以调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>getpid</code></span>函数得到自己的进程id，也可以调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>getppid</code></span>函数得到父进程的id。</li>
<li>在父进程中用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>getpid</code></span>可以得到自己的进程id，然而要想得到子进程的id，只有将<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>的返回值记录下来，别无它法。</li>
</ul>
<p><strong><span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>的另一个特性是所有由父进程打开的描述符都被复制到子进程中。</strong>父、子进程中相同编号的文件描述符在内核中指向同一个<span style="overflow-x: auto; max-width:100%; display:inline;"><code>file</code></span>结构体，也就是说，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>file</code></span>结构体的引用计数要增加。</p>
<h6 id="311-gdb">3.1.1 用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>gdb</code></span>调试多进程</h6>
<p>用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>gdb</code></span>调试多进程的程序会遇到困难，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>gdb</code></span>只能跟踪一个进程（默认是跟踪父进程），而不能同时跟踪多个进程，但可以设置<span style="overflow-x: auto; max-width:100%; display:inline;"><code>gdb</code></span>在<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>之后跟踪父进程还是子进程。</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ gcc main.c -g
$ gdb a.out
GNU gdb 6.8-debian
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later &lt;http://gnu.org/licenses/gpl.html&gt;
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type &quot;show copying&quot;
and &quot;show warranty&quot; for details.
This GDB was configured as &quot;i486-linux-gnu&quot;...
(gdb) l
2 #include &lt;unistd.h&gt;
3 #include &lt;stdio.h&gt;
4 #include &lt;stdlib.h&gt;
5 
6 int main(void)
7 {
8   pid_t pid;
9   char *message;
10    int n;
11    pid = fork();
(gdb) 
12    if(pid&lt;0) {
13      perror(&quot;fork failed&quot;);
14      exit(1);
15    }
16    if(pid==0) {
17      message = &quot;This is the child\n&quot;;
18      n = 6;
19    } else {
20      message = &quot;This is the parent\n&quot;;
21      n = 3;
(gdb) b 17
Breakpoint 1 at 0x8048481: file main.c, line 17.
(gdb) set follow-fork-mode child 
# set follow-fork-mode child 命令设置gdb在fork之后跟踪子进程
# set follow-fork-mode parent 则是跟踪父进程
(gdb) r
Starting program: /home/akaedu/a.out 
This is the parent
[Switching to process 30725]

# 然后用run命令，看到的现象是父进程一直在运行，在(gdb)提示符下打印消息，而子进程被先前设的断点打断了。
Breakpoint 1, main () at main.c:17
17      message = &quot;This is the child\n&quot;;
(gdb) This is the parent
This is the parent
</code></span></code></pre>
<h5 id="32-exec">3.2. exec函数</h5>
<p>用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>创建子进程后执行的是和父进程相同的程序（但有可能执行不同的代码分支），子进程往往要调用一种<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exec</code></span>函数以执行另一个程序。</p>
<p>当进程调用一种<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exec</code></span>函数时，该进程的用户空间代码和数据完全被新程序替换，从新程序的启动例程开始执行。调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exec</code></span>并不创建新进程，所以调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exec</code></span>前后该进程的id并未改变。</p>
<p>其实有六种以<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exec</code></span>开头的函数，统称<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exec</code></span>函数：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;unistd.h&gt;

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
</code></span></code></pre>
<p><strong>这些函数如果调用成功则加载新的程序从启动代码开始执行，不再返回，如果调用出错则返回-1，所以<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exec</code></span>函数只有出错的返回值而没有成功的返回值。</strong>由于<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exec</code></span>函数只有错误返回值，只要返回了一定是出错了，所以不需要判断它的返回值，直接在后面调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>perror</code></span>即可。</p>
<h6 id="321-exec">3.2.1 exec 函数解释</h6>
<p>这些函数原型看起来很容易混，但只要掌握了规律就很好记。</p>
<ul>
<li>不带字母 p（表示path）的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exec</code></span>函数第一个参数必须是程序的相对路径或绝对路径，例如<span style="overflow-x: auto; max-width:100%; display:inline;"><code>"/bin/ls"</code></span>或<span style="overflow-x: auto; max-width:100%; display:inline;"><code>"./a.out"</code></span>，而不能是<span style="overflow-x: auto; max-width:100%; display:inline;"><code>"ls"</code></span>或<span style="overflow-x: auto; max-width:100%; display:inline;"><code>"a.out"</code></span>。</li>
<li>对于带字母 p 的函数：</li>
<li>如果参数中包含/，则将其视为路径名。</li>
<li>否则视为不带路径的程序名，在<span style="overflow-x: auto; max-width:100%; display:inline;"><code>PATH</code></span>环境变量的目录列表中搜索这个程序。</li>
<li>带有字母 l（表示list）的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exec</code></span>函数要求将新程序的每个命令行参数都当作一个参数传给它，命令行参数的个数是可变的，因此函数原型中有<span style="overflow-x: auto; max-width:100%; display:inline;"><code>...</code></span>，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>...</code></span>中的最后一个可变参数应该是<span style="overflow-x: auto; max-width:100%; display:inline;"><code>NULL</code></span>，起 sentinel 的作用。</li>
<li>对于带有字母 v（表示vector）的函数，则应该先构造一个指向各参数的指针数组，然后将该数组的首地址当作参数传给它，数组中的最后一个指针也应该是<span style="overflow-x: auto; max-width:100%; display:inline;"><code>NULL</code></span>，就像<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span>函数的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>argv</code></span>参数或者环境变量表一样。</li>
<li>对于以 e（表示environment）结尾的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exec</code></span>函数，可以把一份新的环境变量表传给它，其他<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exec</code></span>函数仍使用当前的环境变量表执行新程序。</li>
</ul>
<p><strong>事实上，只有<span style="overflow-x: auto; max-width:100%; display:inline;"><code>execve</code></span>是真正的系统调用</strong>，其它五个函数最终都调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>execve</code></span>，所以<span style="overflow-x: auto; max-width:100%; display:inline;"><code>execve</code></span>在man手册第2节，其它函数在man手册第3节。这些函数之间的关系如下图所示。</p>
<p><strong>图 30.5. exec函数族</strong></p>
<p><img class="pure-img" alt="exec函数族" src="https://zromyk.gitee.io/myblog-figurebed/post/Linux C.assets/process.exec.png" /></p>
<h6 id="322">3.2.2 一个完整的例子</h6>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;unistd.h&gt;
#include &lt;stdlib.h&gt;

int main(void)
{
  execlp(&quot;ps&quot;, &quot;ps&quot;, &quot;-o&quot;, &quot;pid,ppid,pgrp,session,tpgid,comm&quot;, NULL);
  perror(&quot;exec ps&quot;);
  exit(1);
}
</code></span></code></pre>
<p>执行此程序则得到：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>$ ./a.out 
  PID  PPID  PGRP  SESS TPGID COMMAND
 6614  6608  6614  6614  7199 bash
 7199  6614  7199  6614  7199 ps
</code></span></code></pre>
<p>由于<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exec</code></span>函数只有错误返回值，只要返回了一定是出错了，所以不需要判断它的返回值，直接在后面调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>perror</code></span>即可。注意在调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>execlp</code></span>时传了两个<span style="overflow-x: auto; max-width:100%; display:inline;"><code>"ps"</code></span>参数，第一个<span style="overflow-x: auto; max-width:100%; display:inline;"><code>"ps"</code></span>是程序名，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>execlp</code></span>函数要在<span style="overflow-x: auto; max-width:100%; display:inline;"><code>PATH</code></span>环境变量中找到这个程序并执行它，而第二个<span style="overflow-x: auto; max-width:100%; display:inline;"><code>"ps"</code></span>是第一个命令行参数，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>execlp</code></span>函数并不关心它的值，只是简单地把它传给<span style="overflow-x: auto; max-width:100%; display:inline;"><code>ps</code></span>程序，<strong><span style="overflow-x: auto; max-width:100%; display:inline;"><code>ps</code></span>程序可以通过<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span>函数的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>argv[0]</code></span>取到这个参数</strong>。</p>
<h5 id="33-waitwaitpid">3.3. wait和waitpid函数</h5>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>wait</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>waitpid</code></span>函数的原型是：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;sys/types.h&gt;
#include &lt;sys/wait.h&gt;

pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
</code></span></code></pre>
<p>若调用成功则返回清理掉的子进程id，若调用出错则返回-1。父进程调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>wait</code></span>或<span style="overflow-x: auto; max-width:100%; display:inline;"><code>waitpid</code></span>时可能会：</p>
<ul>
<li>阻塞（如果它的所有子进程都还在运行）。</li>
<li>带子进程的终止信息立即返回（如果一个子进程已终止，正等待父进程读取其终止信息）。</li>
<li>出错立即返回（如果它没有任何子进程）。</li>
</ul>
<p>这两个函数的区别是：</p>
<ul>
<li>如果父进程的所有子进程都还在运行，调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>wait</code></span>将使父进程阻塞，而调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>waitpid</code></span>时如果在<span style="overflow-x: auto; max-width:100%; display:inline;"><code>options</code></span>参数中指定<span style="overflow-x: auto; max-width:100%; display:inline;"><code>WNOHANG</code></span>可以使父进程不阻塞而立即返回0。</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>wait</code></span>等待第一个终止的子进程，而<span style="overflow-x: auto; max-width:100%; display:inline;"><code>waitpid</code></span>可以通过<span style="overflow-x: auto; max-width:100%; display:inline;"><code>pid</code></span>参数指定等待哪一个子进程。</li>
</ul>
<p>可见，调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>wait</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>waitpid</code></span>不仅可以获得子进程的终止信息，还可以使父进程阻塞等待子进程终止，起到进程间同步的作用。如果参数<span style="overflow-x: auto; max-width:100%; display:inline;"><code>status</code></span>不是空指针，则子进程的终止信息通过这个参数传出，如果只是为了同步而不关心子进程的终止信息，可以将<span style="overflow-x: auto; max-width:100%; display:inline;"><code>status</code></span>参数指定为<span style="overflow-x: auto; max-width:100%; display:inline;"><code>NULL</code></span>。</p>
<p>一个进程在终止时会关闭所有文件描述符，释放在用户空间分配的内存，但它的PCB还保留着，内核在其中保存了一些信息：如果是正常终止则保存着退出状态，如果是异常终止则保存着导致该进程终止的信号是哪个。</p>
<p>这个进程的父进程可以调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>wait</code></span>或<span style="overflow-x: auto; max-width:100%; display:inline;"><code>waitpid</code></span>获取这些信息，然后彻底清除掉这个进程。</p>
<blockquote class="content-quote">
<p>我们知道一个进程的退出状态可以在Shell中用特殊变量<span style="overflow-x: auto; max-width:100%; display:inline;"><code>$?</code></span>查看，因为Shell是它的父进程，当它终止时Shell调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>wait</code></span>或<span style="overflow-x: auto; max-width:100%; display:inline;"><code>waitpid</code></span>得到它的退出状态同时彻底清除掉这个进程。</p>
</blockquote>
<h6 id="331">3.3.1 僵尸进程</h6>
<p>如果一个进程已经终止，但是它的父进程尚未调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>wait</code></span>或<span style="overflow-x: auto; max-width:100%; display:inline;"><code>waitpid</code></span>对它进行清理，这时的进程状态称为僵尸（Zombie）进程。任何进程在刚终止时都是僵尸进程，正常情况下，僵尸进程都立刻被父进程清理了。</p>
<p>为了观察到僵尸进程，我们自己写一个不正常的程序，父进程<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>出子进程，子进程终止，而父进程既不终止也不调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>wait</code></span>清理子进程：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;unistd.h&gt;
#include &lt;stdlib.h&gt;

int main(void)
{
  pid_t pid=fork();
  if(pid&lt;0) {
    perror(&quot;fork&quot;);
    exit(1);
  }
  if(pid&gt;0) { /* parent */
    while(1);
  }
  /* child */
  return 0;   
}
</code></span></code></pre>
<p>在后台运行这个程序，然后用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>ps</code></span>命令查看：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-shell">$ ./a.out &amp;
[1] 6130
$ ps u
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
akaedu    6016  0.0  0.3   5724  3140 pts/0    Ss   08:41   0:00 bash
akaedu    6130 97.2  0.0   1536   284 pts/0    R    08:44  14:33 ./a.out
akaedu    6131  0.0  0.0      0     0 pts/0    Z    08:44   0:00 [a.out] &lt;defunct&gt;
akaedu    6163  0.0  0.0   2620  1000 pts/0    R+   08:59   0:00 ps u
</code></span></code></pre>
<p>父进程的pid是6130，子进程是僵尸进程，pid是6131，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>ps</code></span>命令显示僵尸进程的状态为<span style="overflow-x: auto; max-width:100%; display:inline;"><code>Z</code></span>，在命令行一栏还显示<span style="overflow-x: auto; max-width:100%; display:inline;"><code>&lt;defunct&gt;</code></span>。</p>
<h6 id="332-init">3.3.2 <span style="overflow-x: auto; max-width:100%; display:inline;"><code>init</code></span>进程</h6>
<p>如果一个父进程终止，而它的子进程还存在（这些子进程或者仍在运行，或者已经是僵尸进程了），则这些子进程的父进程改为<span style="overflow-x: auto; max-width:100%; display:inline;"><code>init</code></span>进程。<span style="overflow-x: auto; max-width:100%; display:inline;"><code>init</code></span>是系统中的一个特殊进程，通常程序文件是<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/sbin/init</code></span>，进程id是1，在系统启动时负责启动各种系统服务，之后就负责清理子进程，只要有子进程终止，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>init</code></span>就会调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>wait</code></span>函数清理它。</p>
<p>僵尸进程是不能用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>kill</code></span>命令清除掉的，因为<span style="overflow-x: auto; max-width:100%; display:inline;"><code>kill</code></span>命令只是用来终止进程的，而僵尸进程已经终止了。</p>
<h3 id="4">4. 进程间通信</h3>
<p>每个进程各自有不同的用户地址空间，任何一个进程的全局变量在另一个进程中都看不到，所以进程之间要交换数据必须通过内核，在内核中开辟一块缓冲区，进程1把数据从用户空间拷到内核缓冲区，进程2再从内核缓冲区把数据读走，内核提供的这种机制称为进程间通信（IPC，InterProcess Communication）。如下图所示。</p>
<p><strong>图 30.6. 进程间通信</strong></p>
<p><img class="pure-img" alt="进程间通信" src="https://zromyk.gitee.io/myblog-figurebed/post/Linux C.assets/process.ipc.png" /></p>
<h5 id="41">4.1. 管道</h5>
<p>管道是一种最基本的IPC机制，由<span style="overflow-x: auto; max-width:100%; display:inline;"><code>pipe</code></span>函数创建：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;unistd.h&gt;

int pipe(int filedes[2]);
</code></span></code></pre>
<p>调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>pipe</code></span>函数时在内核中开辟一块缓冲区（称为管道）用于通信，它有一个读端一个写端，然后通过<span style="overflow-x: auto; max-width:100%; display:inline;"><code>filedes</code></span>参数传出给用户程序 <strong>两个文件描述符</strong>：</p>
<ul>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>filedes[0]</code></span>指向管道的读端；</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>filedes[1]</code></span>指向管道的写端（很好记，就像0是标准输入1是标准输出一样）。</li>
</ul>
<p>所以管道在用户程序看起来就像一个打开的文件，通过<span style="overflow-x: auto; max-width:100%; display:inline;"><code>read(filedes[0]);</code></span>或者<span style="overflow-x: auto; max-width:100%; display:inline;"><code>write(filedes[1]);</code></span>向这个文件读写数据其实是在读写内核缓冲区。</p>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>pipe</code></span>函数调用成功返回0，调用失败返回-1。</p>
<p><strong>图 30.7. 管道</strong></p>
<p><img class="pure-img" alt="管道" src="https://zromyk.gitee.io/myblog-figurebed/post/Linux C.assets/process.pipe.png" /></p>
<ol>
<li>父进程调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>pipe</code></span>开辟管道，得到两个文件描述符指向管道的两端。</li>
<li>父进程调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>创建子进程，那么子进程也有两个文件描述符指向同一管道。</li>
<li>父进程关闭管道读端，子进程关闭管道写端。父进程可以往管道里写，子进程可以从管道里读，管道是用环形队列实现的，数据从写端流入从读端流出，这样就实现了进程间通信。</li>
</ol>
<p><strong>例 30.7. 管道</strong></p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;stdlib.h&gt;
#include &lt;unistd.h&gt;
#define MAXLINE 80

int main(void)
{
  int n;
  int fd[2];
  pid_t pid;
  char line[MAXLINE];

  if (pipe(fd) &lt; 0) {
    perror(&quot;pipe&quot;);
    exit(1);
  }
  if ((pid = fork()) &lt; 0) {
    perror(&quot;fork&quot;);
    exit(1);
  }
  if (pid &gt; 0) { /* parent */
    close(fd[0]);
    write(fd[1], &quot;hello world\n&quot;, 12);
    wait(NULL);
  } else {       /* child */
    close(fd[1]);
    n = read(fd[0], line, MAXLINE);
    write(STDOUT_FILENO, line, n);
  }
  return 0;
}
</code></span></code></pre>
<p>使用管道有一些限制：</p>
<ul>
<li>两个进程通过一个管道只能实现单向通信。</li>
<li>管道的读写端通过打开的文件描述符来传递，因此要通信的两个进程必须从它们的公共祖先那里继承管道文件描述符。</li>
</ul>
<p>使用管道需要注意以下4种特殊情况（假设都是阻塞I/O操作，没有设置<span style="overflow-x: auto; max-width:100%; display:inline;"><code>O_NONBLOCK</code></span>标志）：</p>
<ol>
<li>如果所有指向管道写端的文件描述符都关闭了（<strong>管道写端的引用计数等于0</strong>），而仍然有进程从管道的读端读数据，那么管道中剩余的数据都被读取后，再次<span style="overflow-x: auto; max-width:100%; display:inline;"><code>read</code></span>会返回0，就像读到文件末尾一样。</li>
<li>如果有指向管道写端的文件描述符没关闭（<strong>管道写端的引用计数大于0</strong>），而持有管道写端的进程也没有向管道中写数据，这时有进程从管道读端读数据，那么管道中剩余的数据都被读取后，再次<span style="overflow-x: auto; max-width:100%; display:inline;"><code>read</code></span>会阻塞，直到管道中有数据可读了才读取数据并返回。</li>
<li>如果所有指向管道读端的文件描述符都关闭了（<strong>管道读端的引用计数等于0</strong>），这时有进程向管道的写端<span style="overflow-x: auto; max-width:100%; display:inline;"><code>write</code></span>，那么该进程会收到信号<span style="overflow-x: auto; max-width:100%; display:inline;"><code>SIGPIPE</code></span>，通常会导致进程异常终止。在<a href="/public/download/file:///C:/inetpub/wwwroot/book/html-chunk/ch33.html#signal">第 33 章 <em>信号</em></a>会讲到怎样使<span style="overflow-x: auto; max-width:100%; display:inline;"><code>SIGPIPE</code></span>信号不终止进程。</li>
<li>如果有指向管道读端的文件描述符没关闭（<strong>管道读端的引用计数大于0</strong>），而持有管道读端的进程也没有从管道中读数据，这时有进程向管道写端写数据，那么在管道被写满时再次<span style="overflow-x: auto; max-width:100%; display:inline;"><code>write</code></span>会阻塞，直到管道中有空位置了才写入数据并返回。</li>
</ol>
<h5 id="42-ipc">4.2. 其它IPC机制</h5>
<p>现在把进程之间传递信息的各种途径（包括各种IPC机制）总结如下：</p>
<ul>
<li>父进程通过<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>可以将打开文件的描述符传递给子进程。</li>
<li>子进程结束时，父进程调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>wait</code></span>可以得到子进程的终止信息。</li>
<li>几个进程可以在文件系统中读写某个共享文件，也可以通过给文件加锁来实现进程间同步。</li>
<li>进程之间互发信号，一般使用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>SIGUSR1</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>SIGUSR2</code></span>实现用户自定义功能。</li>
<li>管道</li>
<li>FIFO</li>
<li>mmap函数，几个进程可以映射同一内存区。</li>
<li>SYS V IPC，以前的SYS V UNIX系统实现的IPC机制，包括消息队列、信号量和共享内存，<u>现在已经基本废弃</u>。</li>
<li>UNIX Domain Socket，目前最广泛使用的IPC机制。</li>
</ul>
<h3 id="5-shell">5. 练习：实现简单的Shell</h3>
<h2 id="31-shell">31. Shell脚本</h2>
<h3 id="1-shell">1. Shell的历史</h3>
<h3 id="2-shell">2. Shell如何执行命令</h3>
<h5 id="21_2">2.1. 执行交互式命令</h5>
<h5 id="22_1">2.2. 执行脚本</h5>
<h3 id="3-shell">3. Shell的基本语法</h3>
<h5 id="31_1">3.1. 变量</h5>
<h5 id="32-globbing">3.2. 文件名代换（Globbing）：* ? <a href="https://blog.csdn.net/u012365926/article/details/114892795?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-0&amp;spm=1001.2101.3001.4242" title="C++标准库 STL —— 仿函数和适配器"></a></h5>
<h5 id="33">3.3. 命令代换：`或 $()</h5>
<h5 id="34">3.4. 算术代换：$(())</h5>
<p>##### 3.5. 转义字符\</p>
<h5 id="36">3.6. 单引号</h5>
<h5 id="37">3.7. 双引号</h5>
<h3 id="4-bash">4. bash启动脚本</h3>
<h5 id="41-shell-login">4.1. 作为交互登录Shell启动，或者使用&ndash;login参数启动</h5>
<h5 id="42-shell">4.2. 以交互非登录Shell启动</h5>
<h5 id="43">4.3. 非交互启动</h5>
<h5 id="44-sh">4.4. 以sh命令启动</h5>
<h3 id="5-shell_1">5. Shell脚本语法</h3>
<h5 id="51-test">5.1. 条件测试：test [</h5>
<h5 id="52-ifthenelifelsefi">5.2. if/then/elif/else/fi</h5>
<h5 id="53-caseesac">5.3. case/esac</h5>
<h5 id="54-fordodone">5.4. for/do/done</h5>
<h5 id="55-whiledodone">5.5. while/do/done</h5>
<h5 id="56">5.6. 位置参数和特殊变量</h5>
<h5 id="57">5.7. 函数</h5>
<h3 id="6-shell">6. Shell脚本的调试方法</h3>
<h2 id="32">32. 正则表达式</h2>
<h3 id="1_1">1. 引言</h3>
<h3 id="2_1">2. 基本语法</h3>
<h3 id="3-sed">3. sed</h3>
<h3 id="4-awk">4. awk</h3>
<h3 id="5-c">5. 练习：在C语言中使用正则表达式</h3>
<h2 id="33_1">33. 信号</h2>
<h3 id="1_2">1. 信号的基本概念</h3>
<h3 id="2_2">2. 产生信号</h3>
<h5 id="21_3">2.1. 通过终端按键产生信号</h5>
<h5 id="22_2">2.2. 调用系统函数向进程发信号</h5>
<h5 id="23_1">2.3. 由软件条件产生信号</h5>
<h3 id="3_1">3. 阻塞信号</h3>
<h5 id="31_2">3.1. 信号在内核中的表示</h5>
<h5 id="32_1">3.2. 信号集操作函数</h5>
<h5 id="33-sigprocmask">3.3. sigprocmask</h5>
<h5 id="34-sigpending">3.4. sigpending</h5>
<h3 id="4_1">4. 捕捉信号</h3>
<h5 id="41_1">4.1. 内核如何实现信号的捕捉</h5>
<h5 id="42-sigaction">4.2. sigaction</h5>
<h5 id="43-pause">4.3. pause</h5>
<h5 id="44">4.4. 可重入函数</h5>
<h5 id="45-sigatomictvolatile">4.5. sig<em>atomic</em>t类型与volatile限定符</h5>
<h5 id="46-sigsuspend">4.6. 竞态条件与sigsuspend函数</h5>
<h5 id="47-sigchld">4.7. 关于SIGCHLD信号</h5>
<h2 id="34_1">34. 终端、作业控制与守护进程</h2>
<h3 id="1_3">1. 终端</h3>
<h5 id="11">1.1. 终端的基本概念</h5>
<p>每个进程都可以通过一个特殊的设备文件<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/dev/tty</code></span>访问它的控制终端。事实上每个终端设备都对应一个不同的设备文件，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/dev/tty</code></span>提供了一个通用的接口，一个进程要访问它的控制终端既可以通过<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/dev/tty</code></span>也可以通过该终端设备所对应的设备文件来访问。</p>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>ttyname</code></span>函数可以由文件描述符查出对应的文件名，该文件描述符必须指向一个终端设备而不能是任意文件。</p>
<p>下面我们通过实验看一下各种不同的终端所对应的设备文件名。</p>
<p><strong>例 34.1. 查看终端对应的设备文件名</strong></p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>#include &lt;unistd.h&gt;
#include &lt;stdio.h&gt;

int main()
{
    printf(&quot;fd 0: %s\n&quot;, ttyname(0));
    printf(&quot;fd 1: %s\n&quot;, ttyname(1));
    printf(&quot;fd 2: %s\n&quot;, ttyname(2));
    return 0;
}
</code></span></code></pre>
<p>在图形终端窗口下运行这个程序，可能会得到</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>$ ./a.out
fd 0: /dev/pts/0
fd 1: /dev/pts/0
fd 2: /dev/pts/0
</code></span></code></pre>
<p>再开一个终端窗口运行这个程序，可能又会得到</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>$ ./a.out
fd 0: /dev/pts/1
fd 1: /dev/pts/1
fd 2: /dev/pts/1
</code></span></code></pre>
<h5 id="12">1.2. 终端登录过程</h5>
<p>一台PC通常只有一套键盘和显示器，也就是只有一套终端设备，但是可以通过Ctrl-Alt-F1~Ctrl-Alt-F6切换到6个字符终端，相当于有6套虚拟的终端设备，它们共用同一套物理终端设备，对应的设备文件分别是<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/dev/tty1</code></span>~<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/dev/tty6</code></span>，所以称为虚拟终端（Virtual Terminal）。</p>
<p>设备文件<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/dev/tty0</code></span>表示当前虚拟终端，比如切换到Ctrl-Alt-F1的字符终端时<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/dev/tty0</code></span>就表示<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/dev/tty1</code></span>，切换到Ctrl-Alt-F2的字符终端时<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/dev/tty0</code></span>就表示<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/dev/tty2</code></span>，就像<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/dev/tty</code></span>一样也是一个通用的接口，但它不能表示图形终端窗口所对应的终端。</p>
<p><strong>图 34.1. 终端设备模块</strong></p>
<p><img class="pure-img" alt="终端设备模块" src="https://zromyk.gitee.io/myblog-figurebed/post/Linux C.assets/jobs.terminalmodule.png" /></p>
<p>硬件驱动程序负责读写实际的硬件设备，比如从键盘读入字符和把字符输出到显示器，线路规程像一个过滤器，对于某些特殊字符并不是让它直接通过，而是做特殊处理，比如在键盘上按下Ctrl-Z，对应的字符并不会被用户程序的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>read</code></span>读到，而是被线路规程截获，解释成<span style="overflow-x: auto; max-width:100%; display:inline;"><code>SIGTSTP</code></span>信号发给前台进程，通常会使该进程停止。线路规程应该过滤哪些字符和做哪些特殊处理是可以配置的。</p>
<p>终端设备有输入和输出队列缓冲区，如下图所示。</p>
<p><strong>图 34.2. 终端缓冲</strong></p>
<p><img class="pure-img" alt="终端缓冲" src="https://zromyk.gitee.io/myblog-figurebed/post/Linux C.assets/jobs.terminalqueue.png" /></p>
<blockquote class="content-quote">
<p>未完待续&hellip;</p>
</blockquote>
<h5 id="13">1.3. 网络登录过程</h5>
<p>虚拟终端或串口终端的数目是有限的，虚拟终端一般就是<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/dev/tty1</code></span>~<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/dev/tty6</code></span>六个，串口终端的数目也不超过串口的数目。<strong>然而网络终端或图形终端窗口的数目却是不受限制的，这是通过伪终端（Pseudo TTY）实现的。</strong></p>
<p>一套伪终端由一个主设备（PTY Master）和一个从设备（PTY Slave）组成。</p>
<ul>
<li>主设备在概念上相当于键盘和显示器，只不过它不是真正的硬件而是一个内核模块，操作它的也不是用户而是另外一个进程。</li>
<li>从设备和上面介绍的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/dev/tty1</code></span>这样的终端设备模块类似，只不过它的底层驱动程序不是访问硬件而是访问主设备。</li>
</ul>
<p><strong>图 34.3. 伪终端</strong></p>
<p><img class="pure-img" alt="伪终端" src="https://zromyk.gitee.io/myblog-figurebed/post/Linux C.assets/jobs.pseudotty.png" /></p>
<ol>
<li>用户通过<span style="overflow-x: auto; max-width:100%; display:inline;"><code>telnet</code></span>客户端连接服务器。如果服务器配置为独立（Standalone）模式，则在服务器监听连接请求是一个<span style="overflow-x: auto; max-width:100%; display:inline;"><code>telnetd</code></span>进程，<strong>它<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>出一个<span style="overflow-x: auto; max-width:100%; display:inline;"><code>telnetd</code></span>子进程来服务客户端，父进程仍监听其它连接请求。</strong></li>
</ol>
<p>另外一种可能是服务器端由系统服务程序<span style="overflow-x: auto; max-width:100%; display:inline;"><code>inetd</code></span>或<span style="overflow-x: auto; max-width:100%; display:inline;"><code>xinetd</code></span>监听连接请求，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>inetd</code></span>称为 Internet Super-Server，它监听系统中的多个网络服务端口，如果连接请求的端口号和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>telnet</code></span>服务端口号一致，则<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>/<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exec</code></span>一个<span style="overflow-x: auto; max-width:100%; display:inline;"><code>telnetd</code></span>子进程来服务客户端。<span style="overflow-x: auto; max-width:100%; display:inline;"><code>xinetd</code></span>是<span style="overflow-x: auto; max-width:100%; display:inline;"><code>inetd</code></span>的升级版本，配置更为灵活。</p>
<ol start="2">
<li>
<p><span style="overflow-x: auto; max-width:100%; display:inline;"><code>telnetd</code></span>子进程打开一个伪终端设备，然后再经过<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>一分为二：父进程操作伪终端主设备，子进程将伪终端从设备作为它的控制终端，并且将文件描述符0、1、2指向控制终端，二者通过伪终端通信，父进程还负责和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>telnet</code></span>客户端通信，而子进程负责用户的登录过程，提示输入帐号，然后调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exec</code></span>变成<span style="overflow-x: auto; max-width:100%; display:inline;"><code>login</code></span>进程，提示输入密码，然后调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exec</code></span>变成Shell进程。这个Shell进程认为自己的控制终端是伪终端从设备，伪终端主设备可以看作键盘显示器等硬件，而操作这个伪终端的“用户”就是父进程<span style="overflow-x: auto; max-width:100%; display:inline;"><code>telnetd</code></span>。</p>
</li>
<li>
<p>当用户输入命令时，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>telnet</code></span>客户端将用户输入的字符通过网络发给<span style="overflow-x: auto; max-width:100%; display:inline;"><code>telnetd</code></span>服务器，由<span style="overflow-x: auto; max-width:100%; display:inline;"><code>telnetd</code></span>服务器代表用户将这些字符输入伪终端。Shell进程并不知道自己连接的是伪终端而不是真正的键盘显示器，也不知道操作终端的“用户”其实是<span style="overflow-x: auto; max-width:100%; display:inline;"><code>telnetd</code></span>服务器而不是真正的用户。Shell仍然解释执行命令，将标准输出和标准错误输出写到终端设备，这些数据最终由<span style="overflow-x: auto; max-width:100%; display:inline;"><code>telnetd</code></span>服务器发回给<span style="overflow-x: auto; max-width:100%; display:inline;"><code>telnet</code></span>客户端，然后显示给用户看。</p>
</li>
</ol>
<p>如果<span style="overflow-x: auto; max-width:100%; display:inline;"><code>telnet</code></span>客户端和服务器之间的网络延迟较大，我们会观察到按下一个键之后要过几秒钟才能回显到屏幕上。这说明我们 <strong>每按一个键<span style="overflow-x: auto; max-width:100%; display:inline;"><code>telnet</code></span>客户端都会立刻把该字符发送给服务器</strong>，然后这个字符经过伪终端主设备和从设备之后被 Shell 进程读取，同时回显到伪终端从设备，回显的字符再经过伪终端主设备、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>telnetd</code></span>服务器和网络发回给<span style="overflow-x: auto; max-width:100%; display:inline;"><code>telnet</code></span>客户端，显示给用户看。也许你会觉得吃惊，但真的是这样：<strong>每按一个键都要在网络上走个来回！</strong></p>
<h3 id="2_3">2. 作业控制</h3>
<h5 id="21-session">2.1. Session与进程组</h5>
<p>在<a href="/public/download/file:///C:/inetpub/wwwroot/book/html-chunk/ch33s01.html#signal.intro">第 1 节 “信号的基本概念”</a>中我说过“Shell可以同时运行一个前台进程和任意多个后台进程”其实是不全面的，现在我们来研究更复杂的情况。事实上，Shell分前后台来控制的不是进程而是作业（Job）或者进程组（Process Group）。</p>
<p>一个前台作业可以由多个进程组成，一个后台作业也可以由多个进程组成，Shell 可以<strong>同时运行一个前台作业和任意多个后台作业</strong>，这称为作业控制（Job Control）。</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>$ proc1 | proc2 &amp;
$ proc3 | proc4 | proc5
</code></span></code></pre>
<p>其中<span style="overflow-x: auto; max-width:100%; display:inline;"><code>proc1</code></span>和<span style="overflow-x: auto; max-width:100%; display:inline;"><code>proc2</code></span>属于同一个后台进程组，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>proc3</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>proc4</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>proc5</code></span>属于同一个前台进程组，Shell进程本身属于一个单独的进程组。</p>
<p>这些进程组的控制终端相同，它们属于同一个Session。当用户在控制终端输入特殊的控制键（例如Ctrl-C）时，内核会发送相应的信号（例如<span style="overflow-x: auto; max-width:100%; display:inline;"><code>SIGINT</code></span>）<strong>给前台进程组的所有进程</strong>。各进程、进程组、Session的关系如下图所示。</p>
<p><strong>图 34.4. Session与进程组</strong></p>
<p><img class="pure-img" alt="Session与进程组" src="https://zromyk.gitee.io/myblog-figurebed/post/Linux C.assets/jobs.pg.png" /></p>
<h5 id="22_3">2.2. 与作业控制有关的信号</h5>
<p>我们通过实验来理解与作业控制有关的信号。</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>$ cat &amp;
[1] 9386
$ （再次回车） 

[1]+  Stopped                 cat
</code></span></code></pre>
<p>将<span style="overflow-x: auto; max-width:100%; display:inline;"><code>cat</code></span>放到后台运行，由于<span style="overflow-x: auto; max-width:100%; display:inline;"><code>cat</code></span>需要读标准输入（也就是终端输入），而后台进程是不能读终端输入的，因此内核发<span style="overflow-x: auto; max-width:100%; display:inline;"><code>SIGTTIN</code></span>信号给进程，该信号的默认处理动作是使进程停止。</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>$ jobs
[1]+  Stopped                 cat
$ fg %1
cat
hello（回车）
hello
^Z
[1]+  Stopped                 cat

$ bg %1
[1]+ cat &amp;

[1]+  Stopped                 cat
</code></span></code></pre>
<ul>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>jobs</code></span>命令可以查看当前有哪些作业。</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>fg</code></span>命令可以将某个作业提至前台运行，如果该作业的进程组正在后台运行则提至前台运行，如果该作业处于停止状态，则给进程组的每个进程发<span style="overflow-x: auto; max-width:100%; display:inline;"><code>SIGCONT</code></span>信号使它继续运行。</li>
<li>参数<span style="overflow-x: auto; max-width:100%; display:inline;"><code>%1</code></span>表示将第1个作业提至前台运行。</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>cat</code></span>提到前台运行后，挂起等待终端输入，当输入<span style="overflow-x: auto; max-width:100%; display:inline;"><code>hello</code></span>并回车后，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>cat</code></span>打印出同样的一行，然后继续挂起等待输入。如果输入Ctrl-Z则向所有前台进程发<span style="overflow-x: auto; max-width:100%; display:inline;"><code>SIGTSTP</code></span>信号，该信号的默认动作是使进程停止。</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>bg</code></span>命令可以让某个停止的作业在后台继续运行，也需要给该作业的进程组的每个进程发<span style="overflow-x: auto; max-width:100%; display:inline;"><code>SIGCONT</code></span>信号。<span style="overflow-x: auto; max-width:100%; display:inline;"><code>cat</code></span>进程继续运行，又要读终端输入，然而它在后台不能读终端输入，所以又收到<span style="overflow-x: auto; max-width:100%; display:inline;"><code>SIGTTIN</code></span>信号而停止。</li>
</ul>
<h3 id="3_2">3. 守护进程</h3>
<p>Linux系统启动时会启动很多系统服务进程，例如<a href="/public/download/file:///C:/inetpub/wwwroot/book/html-chunk/ch34s01.html#jobs.netlogin">第 1.3 节 “网络登录过程”</a>讲的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>inetd</code></span>，这些系统服务进程没有控制终端，不能直接和用户交互。这种进程有一个名称叫守护进程（Daemon）。</p>
<p>其它进程都是在用户登录或运行程序时创建，在运行结束或用户注销时终止，但系统服务进程不受用户登录注销的影响，它们一直在运行着。</p>
<p>下面我们用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>ps axj</code></span>命令查看系统中的进程。</p>
<ul>
<li>参数<span style="overflow-x: auto; max-width:100%; display:inline;"><code>a</code></span>表示不仅列当前用户的进程，也列出所有其他用户的进程，</li>
<li>参数<span style="overflow-x: auto; max-width:100%; display:inline;"><code>x</code></span>表示不仅列有控制终端的进程，也列出所有无控制终端的进程，</li>
<li>参数<span style="overflow-x: auto; max-width:100%; display:inline;"><code>j</code></span>表示列出与作业控制相关的信息。</li>
</ul>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>$ ps axj
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
    0     1     1     1 ?           -1 Ss       0   0:01 /sbin/init
    0     2     0     0 ?           -1 S&lt;       0   0:00 [kthreadd]
    2     3     0     0 ?           -1 S&lt;       0   0:00 [migration/0]
    2     4     0     0 ?           -1 S&lt;       0   0:00 [ksoftirqd/0]
...
    1  2373  2373  2373 ?           -1 S&lt;s      0   0:00 /sbin/udevd --daemon
...
    1  4680  4680  4680 ?           -1 Ss       0   0:00 /usr/sbin/acpid -c /etc
...
    1  4808  4808  4808 ?           -1 Ss     102   0:00 /sbin/syslogd -u syslog
...
</code></span></code></pre>
<p><strong>凡是<span style="overflow-x: auto; max-width:100%; display:inline;"><code>TPGID</code></span>一栏写着-1的都是没有控制终端的进程，也就是守护进程。</strong></p>
<p>【内核线程】在<span style="overflow-x: auto; max-width:100%; display:inline;"><code>COMMAND</code></span>一列用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>[]</code></span>括起来的名字表示内核线程，这些线程在内核里创建，没有用户空间代码，因此没有程序文件名和命令行，通常采用以<span style="overflow-x: auto; max-width:100%; display:inline;"><code>k</code></span>开头的名字，表示 Kernel。</p>
<ul>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>init</code></span>进程我们已经很熟悉了，</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>udevd</code></span>负责维护<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/dev</code></span>目录下的设备文件，</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>acpid</code></span>负责电源管理，</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>syslogd</code></span>负责维护<span style="overflow-x: auto; max-width:100%; display:inline;"><code>/var/log</code></span>下的日志文件，</li>
</ul>
<p>可以看出，守护进程通常采用以<span style="overflow-x: auto; max-width:100%; display:inline;"><code>d</code></span>结尾的名字，表示 Daemon。</p>
<p>创建守护进程最关键的一步是调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>setsid</code></span>函数创建一个新的 Session，并成为 Session Leader。</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>#include &lt;unistd.h&gt;

pid_t setsid(void);
</code></span></code></pre>
<p>该函数调用成功时返回新创建的 Session 的id（其实也就是当前进程的id），出错返回-1。</p>
<p>注意，调用这个函数之前，当前进程不允许是进程组的Leader，否则该函数返回-1。</p>
<p>要保证当前进程不是进程组的Leader也很容易，只要先<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>再调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>setsid</code></span>就行了。<span style="overflow-x: auto; max-width:100%; display:inline;"><code>fork</code></span>创建的子进程和父进程在同一个进程组中，进程组的 Leader 必然是该组的第一个进程，所以<strong>子进程不可能是该组的第一个进程，在子进程中调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>setsid</code></span>就不会有问题了。</strong></p>
<p>成功调用该函数的结果是：</p>
<ul>
<li>创建一个新的 Session，当前进程成为 Session Leader，当前进程的 id 就是 Session 的 id。</li>
<li>创建一个新的进程组，当前进程成为进程组的 Leader，当前进程的 id 就是进程组的id。</li>
<li>如果当前进程原本有一个控制终端，则它失去这个控制终端，成为一个没有控制终端的进程。所谓失去控制终端是指，原来的控制终端仍然是打开的，仍然可以读写，但只是一个普通的打开文件而不是控制终端了。</li>
</ul>
<h2 id="35">35. 线程</h2>
<h3 id="1_4">1. 线程的概念</h3>
<p>由于同一进程的多个线程共享同一地址空间，因此 Text Segment、Data Segment 都是共享的，如果定义一个函数，在各线程中都可以调用，如果定义一个全局变量，在各线程中都可以访问到，除此之外，各线程还共享以下进程资源和环境：</p>
<ul>
<li>文件描述符表</li>
<li>每种信号的处理方式（<span style="overflow-x: auto; max-width:100%; display:inline;"><code>SIG_IGN</code></span>、<span style="overflow-x: auto; max-width:100%; display:inline;"><code>SIG_DFL</code></span>或者自定义的信号处理函数）</li>
<li>当前工作目录</li>
<li>用户id和组id</li>
</ul>
<p>但有些资源是每个线程各有一份的：</p>
<ul>
<li>线程id</li>
<li>上下文，包括各种寄存器的值、程序计数器和栈指针</li>
<li>栈空间</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>errno</code></span>变量</li>
<li>信号屏蔽字</li>
<li>调度优先级</li>
</ul>
<p>我们将要学习的线程库函数是由 POSIX 标准定义的，称为 POSIX thread 或者 pthread。在Linux 上线程函数位于<span style="overflow-x: auto; max-width:100%; display:inline;"><code>libpthread</code></span>共享库中，因此在编译时要加上<span style="overflow-x: auto; max-width:100%; display:inline;"><code>-lpthread</code></span>选项。</p>
<h3 id="2_4">2. 线程控制</h3>
<h5 id="21_4">2.1. 创建线程</h5>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;pthread.h&gt;

int pthread_create(pthread_t *restrict thread,
  const pthread_attr_t *restrict attr,
  void *(*start_routine)(void*), void *restrict arg);
</code></span></code></pre>
<p>返回值：成功返回0，失败返回错误号。</p>
<blockquote class="content-quote">
<p>以前学过的系统函数都是成功返回0，失败返回-1，而错误号保存在全局变量<span style="overflow-x: auto; max-width:100%; display:inline;"><code>errno</code></span>中。</p>
</blockquote>
<p>在一个线程中调用 pthread<em>create() 创建新的线程后，当前线程从 pthread</em>create() 返回继续往下执行，而新的线程所执行的代码由我们传给<span style="overflow-x: auto; max-width:100%; display:inline;"><code>pthread_create</code></span>的函数指针<span style="overflow-x: auto; max-width:100%; display:inline;"><code>start_routine</code></span>决定。</p>
<ul>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>attr</code></span>参数表示线程属性，本章不深入讨论线程属性，所有代码例子都传<span style="overflow-x: auto; max-width:100%; display:inline;"><code>NULL</code></span>给<span style="overflow-x: auto; max-width:100%; display:inline;"><code>attr</code></span>参数，表示线程属性取缺省值。</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>start_routine</code></span></li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>start_routine</code></span>函数接收一个参数，是通过<span style="overflow-x: auto; max-width:100%; display:inline;"><code>pthread_create</code></span>的<span style="overflow-x: auto; max-width:100%; display:inline;"><code>arg</code></span>参数传递给它的，该参数的类型为<span style="overflow-x: auto; max-width:100%; display:inline;"><code>void *</code></span>，这个指针按什么类型解释由调用者自己定义。</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>start_routine</code></span>的返回值类型也是<span style="overflow-x: auto; max-width:100%; display:inline;"><code>void *</code></span>，这个指针的含义同样由调用者自己定义。</li>
<li><span style="overflow-x: auto; max-width:100%; display:inline;"><code>start_routine</code></span>返回时，这个线程就退出了，其它线程可以调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>pthread_join</code></span>得到<span style="overflow-x: auto; max-width:100%; display:inline;"><code>start_routine</code></span>的返回值，类似于父进程调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>wait(2)</code></span>得到子进程的退出状态，稍后详细介绍<span style="overflow-x: auto; max-width:100%; display:inline;"><code>pthread_join</code></span>。</li>
</ul>
<p>一个简单的例子：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;string.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;pthread.h&gt;
#include &lt;unistd.h&gt;

pthread_t ntid;

void printids(const char *s)
{
  pid_t      pid;
  pthread_t  tid;

  pid = getpid(); 
    // thread_t类型是一个地址值，属于同一进程的多个线程调用getpid(2)可以得到相同的进程号

  tid = pthread_self();
    // 调用pthread_self(3)得到的线程号各不相同。

  printf(&quot;%s pid %u tid %u (0x%x)\n&quot;, s, (unsigned int)pid,
         (unsigned int)tid, (unsigned int)tid);
}

void *thr_fn(void *arg)
{
  printids(arg);
  return NULL;
}

int main(void)
{
  int err;

  err = pthread_create(&amp;ntid, NULL, thr_fn, &quot;new thread: &quot;);
  if (err != 0) {

        // 由于pthread_create的错误码不保存在errno中，
        // 因此不能直接用perror(3)打印错误信息，
        // 可以先用strerror(3)把错误码转换成错误信息再打印。

    fprintf(stderr, &quot;can't create thread: %s\n&quot;, strerror(err));
    exit(1);
  }
  printids(&quot;main thread:&quot;);
  sleep(1);

  return 0;
}
</code></span></code></pre>
<p>编译运行结果如下：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code>$ gcc main.c -lpthread
$ ./a.out
main thread: pid 7398 tid 3084450496 (0xb7d8fac0)
new thread:  pid 7398 tid 3084446608 (0xb7d8eb90)
</code></span></code></pre>
<p>如果任意一个线程调用了<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exit</code></span>或<span style="overflow-x: auto; max-width:100%; display:inline;"><code>_exit</code></span>，则整个进程的所有线程都终止。</p>
<h5 id="22_4">2.2. 终止线程</h5>
<p>如果需要只终止某个线程而不终止整个进程，可以有三种方法：</p>
<ul>
<li>从线程函数<span style="overflow-x: auto; max-width:100%; display:inline;"><code>return</code></span>。<strong>这种方法对主线程不适用，从<span style="overflow-x: auto; max-width:100%; display:inline;"><code>main</code></span>函数<span style="overflow-x: auto; max-width:100%; display:inline;"><code>return</code></span>相当于调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>exit</code></span>。</strong></li>
<li>一个线程可以调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>pthread_cancel</code></span>终止同一进程中的另一个线程。</li>
<li>线程可以调用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>pthread_exit</code></span>终止自己。</li>
</ul>
<h3 id="3_3">3. 线程间同步</h3>
<h5 id="31-mutex">3.1. mutex（互斥锁）</h5>
<p>多个线程同时访问共享数据时可能会冲突，这跟前面讲信号时所说的可重入性是同样的问题。比如两个线程都要把某个全局变量增加1，这个操作在某平台需要三条指令完成：</p>
<ol>
<li>从内存读变量值到寄存器</li>
<li>寄存器的值加1</li>
<li>将寄存器的值写回内存</li>
</ol>
<p>假设两个线程在多处理器平台上同时执行这三条指令，则可能导致下图所示的结果，最后变量只加了一次而非两次。</p>
<p><strong>图 35.1. 并行访问冲突</strong></p>
<p><img class="pure-img" alt="并行访问冲突" src="https://zromyk.gitee.io/myblog-figurebed/post/Linux C.assets/thread.corrupt.png" /></p>
<p>对于多线程的程序，访问冲突的问题是很普遍的，解决的办法是引入<strong>互斥锁（Mutex，Mutual Exclusive Lock）</strong>，获得锁的线程可以完成“读-修改-写”的操作，然后释放锁给其它线程，没有获得锁的线程只能等待而不能访问共享数据，这样“读-修改-写”三步操作组成一个原子操作，要么都执行，要么都不执行，不会执行到中间被打断，也不会在其它处理器上并行做这个操作。</p>
<p>Mutex用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>pthread_mutex_t</code></span>类型的变量表示，可以这样初始化和销毁：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;pthread.h&gt;

// 用pthread_mutex_destroy 销毁 Mutex
int pthread_mutex_destroy(pthread_mutex_t *mutex);

// 用pthread_mutex_init 函数初始化的 Mutex
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
       const pthread_mutexattr_t *restrict attr); 


pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
</code></span></code></pre>
<p>返回值：成功返回0，失败返回错误号。</p>
<p>如果 Mutex 变量是静态分配的（全局变量或<span style="overflow-x: auto; max-width:100%; display:inline;"><code>static</code></span>变量），也可以用宏定义<span style="overflow-x: auto; max-width:100%; display:inline;"><code>PTHREAD_MUTEX_INITIALIZER</code></span>来初始化，相当于用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>pthread_mutex_init</code></span>初始化并且<span style="overflow-x: auto; max-width:100%; display:inline;"><code>attr</code></span>参数为<span style="overflow-x: auto; max-width:100%; display:inline;"><code>NULL</code></span>。</p>
<p>Mutex的加锁和解锁操作可以用下列函数：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;pthread.h&gt;

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
</code></span></code></pre>
<p>返回值：成功返回0，失败返回错误号。</p>
<p>一个线程可以调用 pthread<em>mutex</em>lock 获得 Mutex，如果这时另一个线程已经调用pthread<em>mutex</em>lock 获得了该 Mutex，则当前线程需要挂起等待，直到另一个线程调用pthread<em>mutex</em>unlock 释放 Mutex，当前线程被唤醒，才能获得该 Mutex 并继续执行。</p>
<p>如果一个线程既想获得锁，又不想挂起等待，可以调用pthread<em>mutex</em>trylock，如果Mutex已经被另一个线程获得，这个函数会失败返回EBUSY，而不会使线程挂起等待。</p>
<p>那么lock和unlock的伪代码如下：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">lock:
  if(mutex &gt; 0){
    mutex = 0;
    return 0;
  } else
    挂起等待;
  goto lock;

unlock:
  mutex = 1;
  唤醒等待Mutex的线程;
  return 0;
</code></span></code></pre>
<p>为了实现互斥锁操作，大多数体系结构都提供了swap或exchange指令，该指令的作用是把寄存器和内存单元的数据相交换，由于只有一条指令，保证了原子性，即使是多处理器平台，访问内存的总线周期也有先后，一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。</p>
<blockquote class="content-quote">
<p>互斥锁需要 <strong>保证对 Mutex 变量的读取、判断和修改是原子操作。</strong></p>
</blockquote>
<h5 id="32-condition-variable">3.2. Condition Variable（条件变量）</h5>
<p>线程间的同步还有这样一种情况：线程 A 需要等某个条件成立才能继续往下执行，现在这个条件不成立，线程 A 就阻塞等待，而线程 B 在执行过程中使这个条件成立了，就唤醒线程 A继续执行。在 pthread 库中通过条件变量（Condition Variable）来阻塞等待一个条件，或者唤醒等待这个条件的线程。Condition Variable 用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>pthread_cond_t</code></span>类型的变量表示，可以这样初始化和销毁：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;pthread.h&gt;

int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond,
       const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
</code></span></code></pre>
<p>返回值：成功返回0，失败返回错误号。</p>
<p>和 Mutex 的初始化和销毁类似，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>pthread_cond_init</code></span>函数初始化一个 Condition Variable，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>attr</code></span>参数为<span style="overflow-x: auto; max-width:100%; display:inline;"><code>NULL</code></span>则表示缺省属性，<span style="overflow-x: auto; max-width:100%; display:inline;"><code>pthread_cond_destroy</code></span>函数销毁一个Condition Variable。如果 Condition Variable 是静态分配的，也可以用宏定义<span style="overflow-x: auto; max-width:100%; display:inline;"><code>PTHEAD_COND_INITIALIZER</code></span>初始化，相当于用<span style="overflow-x: auto; max-width:100%; display:inline;"><code>pthread_cond_init</code></span>函数初始化并且<span style="overflow-x: auto; max-width:100%; display:inline;"><code>attr</code></span>参数为<span style="overflow-x: auto; max-width:100%; display:inline;"><code>NULL</code></span>。</p>
<p>Condition Variable 的操作可以用下列函数：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;pthread.h&gt;

int pthread_cond_timedwait(pthread_cond_t *restrict cond,
       pthread_mutex_t *restrict mutex,
       const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond,
       pthread_mutex_t *restrict mutex);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
</code></span></code></pre>
<p>返回值：成功返回0，失败返回错误号。</p>
<p>下面的程序演示了一个 <strong>生产者-消费者</strong> 的例子，生产者生产一个结构体串在链表的表头上，消费者从表头取走结构体。</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;stdlib.h&gt;
#include &lt;pthread.h&gt;
#include &lt;stdio.h&gt;

struct msg {
  struct msg *next;
  int num;
};

struct msg *head;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void *consumer(void *p)
{
  struct msg *mp;

  for (;;) {
    pthread_mutex_lock(&amp;lock);
    while (head == NULL)
      pthread_cond_wait(&amp;has_product, &amp;lock);
    mp = head;
    head = mp-&gt;next;
    pthread_mutex_unlock(&amp;lock);
    printf(&quot;Consume %d\n&quot;, mp-&gt;num);
    free(mp);
    sleep(rand() % 5);
  }
}

void *producer(void *p)
{
  struct msg *mp;
  for (;;) {
    mp = malloc(sizeof(struct msg));
    mp-&gt;num = rand() % 1000 + 1;
    printf(&quot;Produce %d\n&quot;, mp-&gt;num);
    pthread_mutex_lock(&amp;lock);
    mp-&gt;next = head;
    head = mp;
    pthread_mutex_unlock(&amp;lock);
    pthread_cond_signal(&amp;has_product);
    sleep(rand() % 5);
  }
}

int main(int argc, char *argv[]) 
{
  pthread_t pid, cid;  

  srand(time(NULL));
  pthread_create(&amp;pid, NULL, producer, NULL);
  pthread_create(&amp;cid, NULL, consumer, NULL);
  pthread_join(pid, NULL);
  pthread_join(cid, NULL);
  return 0;
}
</code></span></code></pre>
<h5 id="33-semaphore">3.3. Semaphore（信号量）</h5>
<p>Mutex 变量是非 0 即 1 的，可看作一种资源的可用数量，初始化时 Mutex 是 1，表示有一个可用资源，加锁时获得该资源，将 Mutex 减到 0，表示不再有可用资源，解锁时释放该资源，将 Mutex 重新加到 1，表示又有了一个可用资源。</p>
<p>信号量（Semaphore）和 Mutex 类似，表示可用资源的数量，和 Mutex 不同的是这个数量可以大于 1。</p>
<p>本节介绍的是 POSIX semaphore 库函数，详见 sem_overview(7)，<strong>这种信号量不仅可用于同一进程的线程间同步，也可用于不同进程间的同步。</strong></p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;semaphore.h&gt;

int sem_init(sem_t *sem, int pshared, unsigned int value);
/* 
  sem_init() 初始化一个 semaphore 变量，
  pshared 参数为 0 表示信号量用于同一进程的线程间同步
  value 参数表示可用资源的数量，
*/ 

int sem_wait(sem_t *sem);
/*
  调用 sem_wait() 可以获得资源，使 semaphore 的值减1，
  如果调用 sem_wait() 时 semaphore 的值已经是0，则挂起等待。

  如果不希望挂起等待，可以调用 sem_trywait()。
*/

int sem_trywait(sem_t *sem);
int sem_post(sem_t * sem);
/*
  调用sem_post()可以释放资源，使semaphore的值加1，
  同时唤醒挂起等待的线程。
*/
int sem_destroy(sem_t * sem);
</code></span></code></pre>
<h5 id="34_2">3.4. 其它线程间同步机制</h5>
<p>如果共享数据是只读的，那么各线程读到的数据应该总是一致的，不会出现访问冲突。只要有一个线程可以改写数据，就必须考虑线程间同步的问题。</p>
<p>由此引出了 <strong>读者写者锁（Reader-Writer Lock）</strong>的概念，Reader 之间并不互斥，可以同时读共享数据，而 Writer 是独占的（exclusive），在 Writer 修改数据时其它 Reader 或Writer 不能访问数据，可见 Reader-Writer Lock 比 Mutex 具有更好的并发性。</p>
<p>用挂起等待的方式解决访问冲突不见得是最好的办法，因为这样毕竟会影响系统的并发性，在某些情况下解决访问冲突的问题可以尽量避免挂起某个线程，例如 Linux 内核的 Seqlock、RCU（read-copy-update）等机制。</p>
<h3 id="4_2">4. 编程练习</h3>
<p>哲学家就餐问题。这是由计算机科学家Dijkstra提出的经典死锁场景。</p>
<p><img class="pure-img" alt="哲学家问题" src="https://zromyk.gitee.io/myblog-figurebed/post/Linux C.assets/thread.philosopher.png" /></p>
<p>每个哲学家都是一个单独的线程，每个线程循环做以下动作：思考rand()%10秒，然后先拿左手边的筷子再拿右手边的筷子（筷子这种资源可以用mutex表示），有任何一边拿不到就一直等着，全拿到就吃饭rand()%10秒，然后放下筷子。</p>
<h2 id="36-tcpip">36. TCP/IP协议基础</h2>
<h3 id="1-tcpip">1. TCP/IP协议栈与数据包封装</h3>
<h3 id="2-rfc-894">2. 以太网(RFC 894)帧格式</h3>
<h3 id="3-arp">3. ARP数据报格式</h3>
<h3 id="4-ip">4. IP数据报格式</h3>
<h3 id="5-ip">5. IP地址与路由</h3>
<h3 id="6-udp">6. UDP段格式</h3>
<h3 id="7-tcp">7. TCP协议</h3>
<h5 id="71">7.1. 段格式</h5>
<h5 id="72">7.2. 通讯时序</h5>
<h5 id="73">7.3. 流量控制</h5>
<h2 id="37-socket">37. socket编程</h2>
<p>socket 这个词可以表示很多概念：</p>
<ul>
<li>在 TCP/IP 协议中，“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程，“IP地址+端口号”就称为 socket。</li>
<li>在 TCP 协议中，建立连接的两个进程各自有一个 socket 来标识，那么这两个 socket 组成的 socket pair 就唯一标识一个连接。socket 本身有“插座”的意思，因此用来描述网络连接的一对一关系。</li>
<li>TCP/IP 协议最早在 BSD UNIX 上实现，为 TCP/IP 协议设计的应用层编程接口称为 socket API。</li>
</ul>
<h3 id="1_5">1. 预备知识</h3>
<h5 id="11_1">1.1. 网络字节序</h5>
<blockquote class="content-quote">
<p>网络数据流的地址应这样规定：先发出的数据是低地址，后发出的数据是高地址。</p>
</blockquote>
<p>调用以下库函数做网络字节序和主机字节序的转换。</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">#include &lt;arpa/inet.h&gt;

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
</code></span></code></pre>
<p>这些函数名很好记，h表示host，n表示network，l表示32位长整数，s表示16位短整数。例如htonl表示将32位的长整数从主机字节序转换为网络字节序，例如将IP地址转换后准备发送。如果主机是小端字节序，这些函数将参数做相应的大小端转换然后返回，如果主机是大端字节序，这些函数不做转换，将参数原封不动地返回。</p>
<h5 id="12-socket">1.2. socket地址的数据类型及相关函数</h5>
<h3 id="2-tcp">2. 基于TCP协议的网络程序</h3>
<p>下图是基于TCP协议的客户端/服务器程序的一般流程：</p>
<p><strong>图 37.2. TCP协议通讯流程</strong></p>
<p><img class="pure-img" alt="TCP协议通讯流程" src="https://zromyk.gitee.io/myblog-figurebed/post/Linux C.assets/socket.tcpflowchart.png" /></p>
<p>服务器调用 socket()、bind()、listen() 完成初始化后，调用 accept() 阻塞等待，处于监听端口的状态;</p>
<p>客户端调用 socket() 初始化后，调用 connect() 发出 SYN 段并阻塞等待服务器应答，服务器应答一个 SYN-ACK 段，客户端收到后从 connect() 返回，同时应答一个 ACK 段，服务器收到后从 accept() 返回。</p>
<h5 id="21-tcp">2.1. 最简单的TCP网络程序</h5>
<p>下面通过最简单的客户端/服务器程序的实例来学习 socket API。</p>
<p>server.c 的作用是从客户端读字符，然后将每个字符转换为大写并回送给客户端。</p>
<p>下面介绍程序中用到的 socket API，这些函数都在<span style="overflow-x: auto; max-width:100%; display:inline;"><code>sys/socket.h</code></span>中。</p>
<h6 id="211">2.1.1 服务端</h6>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">/* server.c */
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;unistd.h&gt;
#include &lt;sys/socket.h&gt;
#include &lt;netinet/in.h&gt;

#define MAXLINE 80
#define SERV_PORT 8000

int main(void)
{
  struct sockaddr_in servaddr, cliaddr;
  socklen_t cliaddr_len;
  int listenfd, connfd;
  char buf[MAXLINE];
  char str[INET_ADDRSTRLEN];
  int i, n;

  listenfd = socket(AF_INET, SOCK_STREAM, 0);
  /*
    int socket(int family, int type, int protocol);

    socket() 打开一个网络通讯端口:
            如果成功的话，就像 open() 一样返回一个文件描述符，
                应用程序可以像读写文件一样用 read/write 在网络上收发数据，
            如果 socket() 调用出错则返回-1。

        family: 
          对于 IPv4，family 参数指定为 AF_INET。
        type:   
          对于 TCP 协议，type 参数指定为 SOCK_STREAM，表示面向流的传输协议。
          对于 UDP 协议，type 参数指定为 SOCK_DGRAM，表示面向数据报的传输协议。
        protocol:
          略，指定为0即可。
  */
  bzero(&amp;servaddr, sizeof(servaddr)); // 首先将整个结构体清零
  servaddr.sin_family = AF_INET;    // 设置地址类型为 AF_INET
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 
      /* 
        网络地址为 INADDR_ANY,
        这个宏表示本地的任意IP地址，
        因为服务器可能有多个网卡，每个网卡也可能绑定多个IP地址，
        这样设置可以在所有的IP地址上监听，
        直到与某个客户端建立了连接时才确定下来到底用哪个IP地址。
      */
  servaddr.sin_port = htons(SERV_PORT); // 端口号为SERV_PORT，我们定义为8000。

  bind(listenfd, (struct sockaddr *)&amp;servaddr, sizeof(servaddr));
  /*
    int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

    服务器程序所监听的网络地址和端口号通常是固定不变的，
    客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接，
    因此服务器需要调用bind绑定一个固定的网络地址和端口号。

    bind() 成功返回 0，失败返回 -1。

    bind() 的作用是将参数 sockfd 和 myaddr 绑定在一起，
      使 sockfd 这个用于网络通讯的文件描述符监听 myaddr 所描述的地址和端口号。
    myaddr:
      struct sockaddr *是一个通用指针类型;
      myaddr 实际上可以接受多种协议的 sockaddr 结构体，
      而它们的长度各不相同，所以需要第三个参数 addrlen 指定结构体的长度。
  */

  listen(listenfd, 20);
    /*
      int listen(int sockfd, int backlog);

      listen() 声明 sockfd 处于监听状态，
      并且最多允许有 backlog 个客户端处于连接待状态，
      如果接收到更多的连接请求就忽略。

      listen() 成功返回 0，失败返回 -1。
    */

  printf(&quot;Accepting connections ...\n&quot;);
  while (1) {
    cliaddr_len = sizeof(cliaddr);
    connfd = accept(listenfd, 
        (struct sockaddr *)&amp;cliaddr, &amp;cliaddr_len);

        /*
          典型的服务器程序可以同时服务于多个客户端，
          当有客户端发起连接时，服务器调用的 accept() 返回并接受这个连接，
          如果有大量的客户端发起连接而服务器来不及处理，
          尚未accept 的客户端就处于连接等待状态。

          int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

          三方握手完成后，服务器调用 accept() 接受连接，
          如果服务器调用 accept() 时还没有客户端的连接请求，
          就阻塞等待直到有客户端连接上来。

          cliaddr:
            一个传出参数，accept() 返回时传出客户端的地址和端口号。
            如果给 cliaddr 参数传 NULL，表示不关心客户端的地址。

          addrlen:
            一个传入传出参数（value-result argument），
            传入的是调用者提供的缓冲区 cliaddr 的长度，以避免缓冲区溢出问题，
            传出的是客户端地址结构体的实际长度。
                    （有可能没有占满调用者提供的缓冲区）。
        */

    n = read(connfd, buf, MAXLINE);
    printf(&quot;received from %s at PORT %d\n&quot;,
           inet_ntop(AF_INET, &amp;cliaddr.sin_addr, str, sizeof(str)),
           ntohs(cliaddr.sin_port));

    for (i = 0; i &lt; n; i++)
      buf[i] = toupper(buf[i]);
    write(connfd, buf, n);
    close(connfd);
  }
}
</code></span></code></pre>
<p>我们的服务器程序结构是这样的：</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">while (1) {
  cliaddr_len = sizeof(cliaddr);
  connfd = accept(listenfd, 
      (struct sockaddr *)&amp;cliaddr, &amp;cliaddr_len);
  n = read(connfd, buf, MAXLINE);
  ...
  close(connfd);
}
</code></span></code></pre>
<p>整个是一个 while 死循环，每次循环处理一个客户端连接。由于 cliaddr_len 是传入传出参数，每次调用 accept() 之前应该重新赋初值。accept() 的参数 listenfd 是先前的监听文件描述符，而 accept() 的返回值是另外一个文件描述符 connfd，之后与客户端之间就通过这个connfd 通讯，最后关闭 connfd 断开连接，而不关闭 listenfd，再次回到循环开头 listenfd仍然用作 accept 的参数。accept() 成功返回一个文件描述符，出错返回-1。</p>
<h6 id="212">2.1.2 客户端</h6>
<p>client.c 的作用是从命令行参数中获得一个字符串发给服务器，然后接收服务器返回的字符串并打印。</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">/* client.c */
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;unistd.h&gt;
#include &lt;sys/socket.h&gt;
#include &lt;netinet/in.h&gt;

#define MAXLINE 80
#define SERV_PORT 8000

int main(int argc, char *argv[])
{
  struct sockaddr_in servaddr;
  char buf[MAXLINE];
  int sockfd, n;
  char *str;

  if (argc != 2) {
    fputs(&quot;usage: ./client message\n&quot;, stderr);
    exit(1);
  }
  str = argv[1];

  sockfd = socket(AF_INET, SOCK_STREAM, 0);

  bzero(&amp;servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  inet_pton(AF_INET, &quot;127.0.0.1&quot;, &amp;servaddr.sin_addr);
  servaddr.sin_port = htons(SERV_PORT);

  connect(sockfd, (struct sockaddr *)&amp;servaddr, sizeof(servaddr));
  /*
    int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

    客户端需要调用 connect() 连接服务器，
    connect 和 bind 的参数形式一致，
    区别在于 bind 的参数是自己的地址，而 connect 的参数是对方的地址。

    connect()成功返回0，出错返回-1。
  */
  write(sockfd, str, strlen(str));

  n = read(sockfd, buf, MAXLINE);
  printf(&quot;Response from server:\n&quot;);
  write(STDOUT_FILENO, buf, n);

  close(sockfd);
  return 0;
}
</code></span></code></pre>
<p>由于客户端不需要固定的端口号，因此不必调用 bind()，客户端的端口号由内核自动分配。</p>
<p>注意：</p>
<ul>
<li>客户端不是不允许调用 bind()，只是没有必要调用 bind() 固定一个端口号。</li>
<li>服务器也不是必须调用 bind()，但如果服务器不调用 bind()，内核会自动给服务器分配监听端口，每次启动服务器时端口号都不一样，客户端要连接服务器就会遇到麻烦。</li>
</ul>
<h5 id="22_5">2.2. 错误处理与读写控制</h5>
<h5 id="23-client">2.3. 把client改为交互式输入</h5>
<h5 id="24-forkclient">2.4. 使用fork并发处理多个client的请求</h5>
<h5 id="25-setsockopt">2.5. setsockopt</h5>
<h5 id="26-select">2.6. 使用select</h5>
<h3 id="3-udp">3. 基于UDP协议的网络程序</h3>
<p><img class="pure-img" alt="UDP通讯流程" src="https://zromyk.gitee.io/myblog-figurebed/post/Linux C.assets/socket.udpflowchart.png" /></p>
<p>以下是简单的UDP服务器和客户端程序。</p>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">/* server.c */
#include &lt;stdio.h&gt;
#include &lt;string.h&gt;
#include &lt;netinet/in.h&gt;
#include &quot;wrap.h&quot;

#define MAXLINE 80
#define SERV_PORT 8000

int main(void)
{
  struct sockaddr_in servaddr, cliaddr;
  socklen_t cliaddr_len;
  int sockfd;
  char buf[MAXLINE];
  char str[INET_ADDRSTRLEN];
  int i, n;

  sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

  bzero(&amp;servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servaddr.sin_port = htons(SERV_PORT);

  Bind(sockfd, (struct sockaddr *)&amp;servaddr, sizeof(servaddr));

  printf(&quot;Accepting connections ...\n&quot;);
  while (1) {
    cliaddr_len = sizeof(cliaddr);
    n = recvfrom(sockfd, buf, MAXLINE, 0, (struct sockaddr *)&amp;cliaddr, &amp;cliaddr_len);
    if (n == -1)
      perr_exit(&quot;recvfrom error&quot;);
    printf(&quot;received from %s at PORT %d\n&quot;,
           inet_ntop(AF_INET, &amp;cliaddr.sin_addr, str, sizeof(str)),
           ntohs(cliaddr.sin_port));

    for (i = 0; i &lt; n; i++)
      buf[i] = toupper(buf[i]);
    n = sendto(sockfd, buf, n, 0, (struct sockaddr *)&amp;cliaddr, sizeof(cliaddr));
    if (n == -1)
      perr_exit(&quot;sendto error&quot;);
  }
}
</code></span></code></pre>
<pre><code class="pre-wrap"><span style="overflow-x: auto; max-width:100%; display:inline;"><code class="language-c">/* client.c */
#include &lt;stdio.h&gt;
#include &lt;string.h&gt;
#include &lt;unistd.h&gt;
#include &lt;netinet/in.h&gt;
#include &quot;wrap.h&quot;

#define MAXLINE 80
#define SERV_PORT 8000

int main(int argc, char *argv[])
{
  struct sockaddr_in servaddr;
  int sockfd, n;
  char buf[MAXLINE];
  char str[INET_ADDRSTRLEN];
  socklen_t servaddr_len;

  sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

  bzero(&amp;servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  inet_pton(AF_INET, &quot;127.0.0.1&quot;, &amp;servaddr.sin_addr);
  servaddr.sin_port = htons(SERV_PORT);

  while (fgets(buf, MAXLINE, stdin) != NULL) {
    n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&amp;servaddr, sizeof(servaddr));
    if (n == -1)
      perr_exit(&quot;sendto error&quot;);

    n = recvfrom(sockfd, buf, MAXLINE, 0, NULL, 0);
    if (n == -1)
      perr_exit(&quot;recvfrom error&quot;);

    Write(STDOUT_FILENO, buf, n);
  }

  Close(sockfd);
  return 0;
}
</code></span></code></pre>
<p>由于UDP不需要维护连接，程序逻辑简单了很多，但是UDP协议是不可靠的，实际上有很多保证通讯可靠性的机制需要在应用层实现。</p>
<p>编译运行server，在两个终端里各开一个client与server交互，看看server是否具有并发服务的能力。用Ctrl+C关闭server，然后再运行server，看此时client还能否和server联系上。和前面TCP程序的运行结果相比较，体会无连接的含义。</p>
<h3 id="4-unix-domain-socket-ipc">4. UNIX Domain Socket IPC</h3>
<h3 id="5-web">5. 练习：实现简单的Web服务器</h3>
<h5 id="51-http">5.1. 基本HTTP协议</h5>
<h5 id="52-cgi">5.2. 执行CGI程序</h5>
  </div>
</div>
 
    </div>
  </div>
  <div id="footer-background">
    <div id="footer">
      <div class="legal pure-g">
  <div class="pure-u-1 u-sm-1-2">
    <p class="legal-license"><a href="https://beian.miit.gov.cn/#/Integrated/index">浙ICP备2020038748号</a></p>
  </div>
  <div class="pure-u-1 u-sm-1-2">
    <p class="legal-links"><a href="https://github.com/zromyk">GitHub</a></p>
    <p class="legal-copyright">Copyright © 2021 Wei Zhou. 保留所有权利。</p>
  </div>
</div>
    </div>
  </div>
  <!-- <script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script> -->
  <script src="/style/html/jquery.min.js"></script>
  <script src='/style/article/latex/latest.js?config=TeX-MML-AM_CHTML'></script>
<!-- <script src="https://cdn.geogebra.org/apps/deployggb.js"></script> -->
<script src="/style/article/deployggb.js"></script>
<!-- <script src="https://apps.bdimg.com/libs/highlight.js/9.1.0/highlight.min.js"></script> -->
<script type="text/javascript">
  // 脚本：navigation 随鼠标移动自动变换宽度
  var element = document.getElementById("navigation"); // 获取要操作的元素
  var elementWidth = parseInt(getComputedStyle(element).width);
  var elementLeft = 0;
  var elementRight = 0;
  element.addEventListener('mouseenter', function (event) { // 添加鼠标按下事件的监听器
    elementLeft = element.getBoundingClientRect().left - 10;
    elementRight = element.getBoundingClientRect().left + elementWidth * 3;
    window.addEventListener('mousemove', resize); // 添加全局的鼠标移动事件的监听器
  });

  function resize(event) {
    var minWidth = elementWidth;
    var maxWidth = elementWidth * 2.5;
    // console.log(elementLeft, event.clientX, elementRight, event.clientX - elementLeft + elementWidth / 2);
    if (elementLeft <= event.clientX && event.clientX <= elementRight) {
      var width = event.clientX - elementLeft + elementWidth / 2;
      width = Math.min(width, maxWidth);
      width = Math.max(width, minWidth);
      element.style.width = width + 'px'; // 设置新的宽度样式属性
    }
    else {
      element.style.width = elementWidth + 'px'; // 设置新的宽度样式属性
      stopResize();
    }
  }

  function stopResize() {
    element.style.width = elementWidth + 'px'; // 设置新的宽度样式属性
    // console.log("stopResize", elementLeft, event.clientX, elementRight, event.clientX - elementLeft + elementWidth / 2);
    window.removeEventListener('mousemove', resize); // 移除鼠标移动事件的监听器
  }
</script>
<script src="/style/article/highlight/highlight.min.js"></script>
<script type="text/javascript">
  // 脚本：code语法高亮
  hljs.initHighlightingOnLoad();
</script>
<script>
  function animateByNav() {
    $("html").animate({
        scrollTop: ($(event.target.hash).offset().top - 52)
    }, 300);
  };
</script>
<script src="/style/article/pell-1.0.6/dist/pell.js"></script>
<script>
  // 脚本：自由编辑页面
  var editor = window.pell.init({
    element: document.getElementById('editor'),
    defaultParagraphSeparator: 'p',
    onChange: function(html) {
        document.getElementById('text-output').innerHTML = html
        document.getElementById('html-output').textContent = html
    }
  });

  function markdownEditor() {
    var articles = document.getElementById('content-articles-markdown');
    if (articles.getAttribute("contenteditable") == "true") {
        articles.setAttribute("contenteditable", "false");
        document.getElementById("content-articles-markdownEditor").style.display = "none"; //隐藏
        document.getElementById("button-markdownEditor").innerHTML = "启用编辑";
    } else {
        articles.setAttribute("contenteditable", "true");
        document.getElementById("content-articles-markdownEditor").style.display = ""; //显示
        document.getElementById("button-markdownEditor").innerHTML = "关闭编辑";
    }
  };

  function save() {
      window.alert("保存成功");
  };
</script>

</body>
</html>
